Elasticsearch:使用 Streamlit、语义搜索和命名实体提取开发 Elastic Search 应用程序

作者:Camille Corti-Georgiou

介绍

一切都是一个搜索问题。 我在 Elastic 工作的第一周就听到有人说过这句话,从那时起,这句话就永久地印在了我的脑海中。 这篇博客的目的并不是我出色的同事对我所做的相关陈述进行分析,但我首先想花点时间剖析一下这个陈述。

自成立以来,Elasticsearch 一直处于技术前沿 - 打破软件领域的模式,为世界各地家喻户晓的公司的技术支柱提供动力。 我们倾向于将 Elastic 的产品分为几个 "OTB" 解决方案 - 安全性、可观察性等,但剥离这些,我们正在解决的问题基本上是面向搜索的。 它是关于能够询问你的数据问题并返回有意义且相关的结果,无论横幅如何,正是这一点使 Elastic 成为一项如此强大的技术。

作为对搜索的颂歌和 Elastic 的一些功能的展示,本博客将带你完成搜索应用程序的端到端开发,使用机器学习模型进行命名实体提取 (NER) 和语义搜索; 将 Elastic 和非 Elastic 组件结合起来,并通过简单的 UI 对它们进行分层,以展示搜索的强大功能。

该指南专为与 Elastic Cloud 一起使用而设计,但是,相同的方法也可以应用于本地托管的实例,只需更改身份验证方法和其他特定于云的概念。 完整的 git 存储库位于:Kibana Search Project

涵盖的主题

  • Logstash
  • 摄取管道
  • ELSER
  • 自定义机器学习模型
  • Streamlit

注意:为了演示方便,在今天的展示中,我将使用本地资管理的 Elasticsearch 来进行演示。在下面的展示中,我们将使用 Elastic Stack 8.12 来进行展示。

第 1 步:从 Kaggle 下载 BBC 新闻数据集

我们将使用的数据集是 BBC 新闻数据集,可从 BBC 新闻数据集以 CSV 形式获取。 这是一个自动更新的数据集,收集来自 BBC 新闻的 RSS 源。 该数据集包括已发表文章的标题、描述、日期、url 和各种其他属性。 我们将使用 Logstash 来提取数据,但是,其他方法(即 Python 客户端或标准上传)同样有效(将数据添加到 Elasticsearch | Elasticsearch 服务文档 | Elastic)。

原始数据模式有点问题,因此,如果使用 Logstash,需要对文件结构进行细微调整。 解压缩下载的文件并运行脚本,修改输入/输出以反映本地计算机上保存的文件的位置和名称。 该脚本将对列重新排序,使文章的 "Publish Date" 排在第一位,以方便对日期字段的解释。

convert.py

python 复制代码
1.  import csv

3.  input_csv = 'path/bbc_news.csv'
4.  output_csv = 'path/new-bbc_news.csv'

6.  # Read in the old bbc_news CSV file
7.  with open(input_csv, 'r') as infile:
8.      reader = csv.DictReader(infile)
9.      data = [row for row in reader]

11.  # Write the file in the desired format
12.  with open(output_csv, 'w', newline='') as outfile:
13.      fieldnames = ['pubDate', 'title', 'guid', 'link', 'description']
14.      writer = csv.DictWriter(outfile, fieldnames=fieldnames)

16.      writer.writeheader()

18.      # Write data in new format
19.      for row in data:
20.          writer.writerow({
21.              'pubDate': row['pubDate'],
22.              'title': row['title'],
23.              'guid': row['guid'],
24.              'link': row['link'],
25.              'description': row['description']
26.          })

28.  print(f'Success. Output saved to {output_csv}')
bash 复制代码
1.  $ pwd
2.  /Users/liuxg/data/bbs
3.  $ ls
4.  bbc_news.csv bbs.zip      convert.py
5.  $ python convert.py 
6.  Success. Output saved to ./new-bbc_news.csv
7.  $ cat new-bbc_news.csv 
8.  pubDate,title,guid,link,description
9.  "Mon, 07 Mar 2022 08:01:56 GMT",Ukraine: Angry Zelensky vows to punish Russian atrocities,https://www.bbc.co.uk/news/world-europe-60638042,https://www.bbc.co.uk/news/world-europe-60638042?at_medium=RSS&at_campaign=KARANGA,The Ukrainian president says the country will not forgive or forget those who murder its civilians.

步骤 2:使用 Docker 上传并启动自定义 NER ML 模型

接下来,我们将为 NER 任务导入自定义 ML 模型,详细文档可以在此处找到:如何部署命名实体识别 | Elastic Stack 中的机器学习 [8.11]。 本教程使用 docker 上传自定义模型,但是,有关其他安装方法,请参阅:自定义机器学习模型和地图简介 | Elastic 博客。你也可以参考文章 "Elasticsearch:如何部署 NLP:命名实体识别 (NER) 示例"。

虽然我们可以使用许多模型来进行 NER,但我们将使用来自 distilbert-base-uncased · Hugging Face 的 "distilbert-base-uncased"。 该模型针对小写文本进行了优化,事实证明有利于从非结构化数据中精确提取实体,在我们的例子中 - 在命名实体识别的帮助下,我们可以从新闻文章中提取人物、地点、组织等,以供下游使用。

要为此任务创建一次性 API 密钥,我们可以调用 _security 端点,指定我们的密钥要求 API 密钥生成。 确保复制请求生成的编码值,因为以后无法检索该值。 我们创建的 API 密钥将仅用于此上传,因此我们可以分配有限的权限和到期日期:

bash 复制代码
1.  POST /_security/api_key
2.  {
3.    "name": "ml-upload-key",
4.    "expiration": "2d",
5.    "role_descriptors": {
6.      "ml_admin": {
7.        "cluster": ["manage_ml", "monitor"],
8.        "index": [
9.          {
10.            "names": ["*"],
11.            "privileges": ["write", "read", "view_index_metadata"]
12.          }
13.        ]
14.      }
15.    }
16.  }

要将模型导入集群,请确保 Docker Desktop 已启动并正在运行,并在终端中运行以下命令; 设置 "CLOUD_ID" 和 "API_KEY" 的值以反映与你的云集群关联的值。

lua 复制代码
1.  docker run -it --rm docker.elastic.co/eland/eland:latest \
2.      eland_import_hub_model \
3.        --cloud-id $CLOUD_ID \
4.        --es-api-key $API_KEY \
5.        --hub-model-id "elastic/distilbert-base-uncased-finetuned-conll03-english" \
6.        --task-type ner \
7.        --start

如果遇到错误,请确保您的 Cloud ID 和身份验证凭据正确,并且 Docker 按预期运行。

针对我们的自签名的 Elasticsearch 集群,我们可以使用如下的命令来进行:

arduino 复制代码
1.  docker run -it --rm docker.elastic.co/eland/eland:latest \
2.      eland_import_hub_model \
3.        --url https://192.168.0.3:9200/ \
4.        --es-api-key RG9WU0NZNEJ1ODV6ZzUtNllLa3E6UmxQR1lSaVJTeE96TzdPZ05EdzN5dw== \
5.        --hub-model-id "elastic/distilbert-base-uncased-finetuned-conll03-english" \
6.        --task-type ner \
7.        --insecure \
8.        --start

我们可以在 Kibana 中进行查看:

从上面,我们可以看出来上传的模型已经成功地被部署了。

第 3 步:下载 ELSER

在此步骤中,我们将把 Elastic 的 "域外模型" ELSER 下载到堆栈中。 导航到 Machine Learning -> Model Management -> Trained Models 并选择 elser_model_2 上的下载。 有关在非云环境中安装 ELSER 的更多信息,请访问:ELSER -- Elastic Learned Sparse EncodeR | Elastic Stack 中的机器学习 [8.12]

针对本地部署的 Elasticsearch,你可以参考文章 "Elasticsearch:部署 ELSER - Elastic Learned Sparse EncoderR"。你也可以参考文章 "Elastic Search:构建语义搜索体验"。

最终,我们可以看到如下的画面:

第 4 步:在 Elastic 中添加映射和管道

Elastic 中的映射定义了数据的 schema。 我们需要为 BBC 新闻索引添加正式映射,以确保数据按预期键入,并且当我们将数据发送到集群时,Elastic 能够理解其结构。 作为此映射的一部分,我们排除了 ELSER 模型生成的标记以防止映射爆炸,并定义了 NER 模型生成的许多标签。 导航到开发工具并创建映射:

json 复制代码
1.  PUT bbc-news-elser
2.  {
3.      "mappings": {
4.        "_source": {
5.          "excludes": [
6.            "ml-elser-title.tokens",
7.            "ml-elser-description.tokens"
8.          ]
9.        },
10.        "properties": {
11.          "@timestamp": {
12.            "type": "date"
13.          },
14.          "@version": {
15.            "type": "text",
16.            "fields": {
17.              "keyword": {
18.                "type": "keyword",
19.                "ignore_above": 256
20.              }
21.            }
22.          },
23.          "description": {
24.            "type": "text",
25.            "fields": {
26.              "keyword": {
27.                "type": "keyword",
28.                "ignore_above": 256
29.              }
30.            }
31.          },
32.          "event": {
33.            "properties": {
34.              "original": {
35.                "type": "text",
36.                "fields": {
37.                  "keyword": {
38.                    "type": "keyword",
39.                    "ignore_above": 256
40.                  }
41.                }
42.              }
43.            }
44.          },
45.          "ml": {
46.            "properties": {
47.              "ner": {
48.                "properties": {
49.                  "entities": {
50.                    "properties": {
51.                      "class_name": {
52.                        "type": "text",
53.                        "fields": {
54.                          "keyword": {
55.                            "type": "keyword",
56.                            "ignore_above": 256
57.                          }
58.                        }
59.                      },
60.                      "class_probability": {
61.                        "type": "float"
62.                      },
63.                      "end_pos": {
64.                        "type": "long"
65.                      },
66.                      "entity": {
67.                        "type": "text",
68.                        "fields": {
69.                          "keyword": {
70.                            "type": "keyword",
71.                            "ignore_above": 256
72.                          }
73.                        }
74.                      },
75.                      "start_pos": {
76.                        "type": "long"
77.                      }
78.                    }
79.                  },
80.                  "model_id": {
81.                    "type": "text",
82.                    "fields": {
83.                      "keyword": {
84.                        "type": "keyword",
85.                        "ignore_above": 256
86.                      }
87.                    }
88.                  },
89.                  "predicted_value": {
90.                    "type": "text",
91.                    "fields": {
92.                      "keyword": {
93.                        "type": "keyword",
94.                        "ignore_above": 256
95.                      }
96.                    }
97.                  }
98.                }
99.              }
100.            }
101.          },
102.          "ml-elser-description": {
103.            "properties": {
104.              "model_id": {
105.                "type": "text",
106.                "fields": {
107.                  "keyword": {
108.                    "type": "keyword",
109.                    "ignore_above": 256
110.                  }
111.                }
112.              },
113.              "tokens": {
114.                "type": "rank_features"
115.              }
116.            }
117.          },
118.          "ml-elser-title": {
119.            "properties": {
120.              "model_id": {
121.                "type": "text",
122.                "fields": {
123.                  "keyword": {
124.                    "type": "keyword",
125.                    "ignore_above": 256
126.                  }
127.                }
128.              },
129.              "tokens": {
130.                "type": "rank_features"
131.              }
132.            }
133.          },
134.          "pubDate": {
135.            "type": "date",
136.            "format": "EEE, dd MMM yyyy HH:mm:ss 'GMT'",
137.            "ignore_malformed": true
138.          },
139.          "tags": {
140.            "properties": {
141.              "LOC": {
142.                "type": "text",
143.                "fields": {
144.                  "keyword": {
145.                    "type": "keyword",
146.                    "ignore_above": 256
147.                  }
148.                }
149.              },
150.              "MISC": {
151.                "type": "text",
152.                "fields": {
153.                  "keyword": {
154.                    "type": "keyword",
155.                    "ignore_above": 256
156.                  }
157.                }
158.              },
159.              "ORG": {
160.                "type": "text",
161.                "fields": {
162.                  "keyword": {
163.                    "type": "keyword",
164.                    "ignore_above": 256
165.                  }
166.                }
167.              },
168.              "PER": {
169.                "type": "text",
170.                "fields": {
171.                  "keyword": {
172.                    "type": "keyword",
173.                    "ignore_above": 256
174.                  }
175.                }
176.              }
177.            }
178.          },
179.          "title": {
180.            "type": "text",
181.            "fields": {
182.              "keyword": {
183.                "type": "keyword",
184.                "ignore_above": 256
185.              }
186.            }
187.          },
188.          "url": {
189.            "type": "text",
190.            "fields": {
191.              "keyword": {
192.                "type": "keyword",
193.                "ignore_above": 256
194.              }
195.            }
196.          }
197.        }
198.      }
199.    }

Pipelines 在索引之前定义了一系列数据处理步骤。 我们的摄取管道包括字段删除、ELSER 和自定义 NER 模型的模型推理,以及将 NER 模型运行的输出值添加到标签字段的脚本。

bash 复制代码
1.  PUT _ingest/pipeline/news-pipeline
2.  {
3.      "processors": [
4.        {
5.          "remove": {
6.            "field": [
7.              "host",
8.              "message",
9.              "log",
10.              "@version"
11.            ],
12.            "ignore_missing": true
13.          }
14.        },
15.        {
16.          "inference": {
17.            "model_id": "elastic__distilbert-base-uncased-finetuned-conll03-english",
18.            "target_field": "ml.ner",
19.            "field_map": {
20.              "title": "text_field"
21.            }
22.          }
23.        },
24.        {
25.          "script": {
26.            "lang": "painless",
27.            "if": "return ctx['ml']['ner'].containsKey('entities')",
28.            "source": "Map tags = new HashMap(); for (item in ctx['ml']['ner']['entities']) { if (!tags.containsKey(item.class_name)) tags[item.class_name] = new HashSet(); tags[item.class_name].add(item.entity);} ctx['tags'] = tags;"
29.          }
30.        },
31.        {
32.          "inference": {
33.            "model_id": ".elser_model_2",
34.            "target_field": "ml-elser-title",
35.            "field_map": {
36.              "title": "text_field"
37.            },
38.            "inference_config": {
39.              "text_expansion": {
40.                "results_field": "tokens"
41.              }
42.            }
43.          }
44.        },
45.        {
46.          "inference": {
47.            "model_id": ".elser_model_2",
48.            "target_field": "ml-elser-description",
49.            "field_map": {
50.              "description": "text_field"
51.            },
52.            "inference_config": {
53.              "text_expansion": {
54.                "results_field": "tokens"
55.              }
56.            }
57.          }
58.        }
59.      ]
60.    }

第 5 步:使用 Logstash 提取数据

我们现在需要配置 Logstash 将数据发送到 Elastic。 下载 Logstash(如果尚未下载),然后按照此处记录的步骤进行安装:Logstash 入门

markdown 复制代码
1.  $ pwd
2.  /Users/liuxg/elastic
3.  $ ls
4.  elasticsearch-8.12.0                       kibana-8.12.0
5.  elasticsearch-8.12.0-darwin-aarch64.tar.gz kibana-8.12.0-darwin-aarch64.tar.gz
6.  enterprise-search-8.12.1                   logstash-8.12.0-darwin-aarch64.tar.gz
7.  enterprise-search-8.12.1.tar.gz            metricbeat-8.12.0-darwin-aarch64.tar.gz
8.  filebeat-8.12.0-darwin-aarch64.tar.gz
9.  $ tar xzf logstash-8.12.0-darwin-aarch64.tar.gz 
10.  $ cd logstash-8.12.0
11.  $ touch logstash.conf
12.  $ ls logstash.conf
13.  logstash.conf

我们将编辑文件 logstash.conf 作为 Logstash 的配置文件。

我们的配置文件包含三个元素:输入块、过滤器块和输出块。 让我们花一点时间来浏览一下每个内容。

Input:我们的输入将 Logstash 配置为从位于指定路径的 CSV 文件中读取数据。 它从文件的开头开始读取,禁用sincedb 功能,并假设文件是纯文本形式。

ini 复制代码
1.  input {
2.    file {
3.      path => "/path_to_file/new-bbc_news.csv"
4.      start_position => "beginning"
5.      sincedb_path => "/dev/null"
6.      codec => "plain"
7.    }
8.  }

filter:此部分对传入数据应用过滤器。 它使用 CSV 过滤器来解析 CSV 数据,指定逗号作为分隔符并定义列名称。 为了解决 BBC 新闻数据集中存在重复条目的问题,我们应用指纹过滤器根据 "title" 和 "link" 字段的串联来计算唯一指纹,并将其存储在 [@metadata][fingerprint] 中。 mutate 过滤器将 "link" 字段重命名为 "url" 并删除 "guid" 字段。

ini 复制代码
1.  filter {
2.    csv {
3.      separator => ","
4.      columns => ["pubDate", "title", "guid", "link", "description"]
5.      skip_header => true
6.      quote_char => '"'

8.    }

10.    fingerprint {
11.      source => ["title", "link"]
12.      target => "[@metadata][fingerprint]"
13.  }

15.    mutate { rename => { "link" => "url" } }
16.  }

ouput:最后一部分配置处理后数据的输出目的地。 它将数据发送到由 Cloud ID 和凭证指定的 Elasticsearch Cloud 实例。 数据存储在 "bbc-news-elser" 索引中(在第 2 节中映射),并且应用了名为 "news-pipeline" 的摄取管道。 document_id 设置为我们的指纹过滤器生成的唯一指纹。 此外,使用 rubydebug 编解码器将数据的副本打印到控制台以进行调试。

python 复制代码
1.  output {
2.    elasticsearch {
3.      cloud_id => "${CLOUD_ID}"
4.      api_key => ${API_KEY}"
5.      index => "bbc-news-elser"
6.      pipeline => "news-pipeline"
7.      document_id => "%{[@metadata][fingerprint]}"

9.    }
10.    stdout { codec => rubydebug }
11.  }

请记住将 CLOUD_ID 和 API_KEY 设置为环境变量 - 或存储在密钥存储中,Logstash Keystore Guide - 并确保 CSV 文件的路径准确。 注意 - 你需要为 Logstash 创建一个具有相关权限的新 API 密钥。 你可以使用 "-f" 标志直接从命令行运行 Logstash 来指定配置位置,也可以使用管道文件指向配置。 如果选择管道方法,请将以下行添加到 pipelines.yml 文件中:

lua 复制代码
1.  - pipeline.id: bbc-news
2.    path.config: "path-to-config"

针对我们的情况,我们使用本地部署的 Elasticsearch。我们可以详细参考文章 "Logstash:如何连接到带有 HTTPS 访问的集群"。

ruby 复制代码
1.  $ ./bin/elasticsearch-keystore list
2.  keystore.seed
3.  xpack.security.http.ssl.keystore.secure_password
4.  xpack.security.transport.ssl.keystore.secure_password
5.  xpack.security.transport.ssl.truststore.secure_password
6.  $ ./bin/elasticsearch-keystore show xpack.security.http.ssl.keystore.secure_password
7.  Yx33RxJsQmakbbZR4bjlew
8.  $ cd config/certs/
9.  $ ls
10.  http.p12      http_ca.crt   transport.p12
11.  $ keytool -import -file http_ca.crt -keystore truststore.p12 -storepass password -noprompt -storetype pkcs12
12.  Certificate was added to keystore
13.  $ ls
14.  http.p12       http_ca.crt    transport.p12  truststore.p12
15.  $ keytool -keystore truststore.p12 -list
16.  Enter keystore password:  
17.  Keystore type: PKCS12
18.  Keystore provider: SUN

20.  Your keystore contains 1 entry

22.  mykey, Mar 4, 2024, trustedCertEntry, 
23.  Certificate fingerprint (SHA-256): BC:E6:6E:D5:50:97:F2:55:FC:8E:44:20:BD:AD:AF:C8:D6:09:CC:80:27:03:8C:2D:D0:9D:80:56:68:F3:45:9E

我们为 logstash 的摄取获得一个 api-key:

我们完整的 logstash.conf 文件为:

logstash.conf

ini 复制代码
1.  input {
2.    file {
3.      path => "/Users/liuxg/data/bbs/new-bbc_news.csv"
4.      start_position => "beginning"
5.      sincedb_path => "/dev/null"
6.      codec => "plain"
7.    }
8.  }

10.  filter {
11.    csv {
12.      separator => ","
13.      columns => ["pubDate", "title", "guid", "link", "description"]
14.      skip_header => true
15.      quote_char => '"'

17.    }

19.    fingerprint {
20.      source => ["title", "link"]
21.      target => "[@metadata][fingerprint]"
22.  }

24.    mutate { rename => { "link" => "url" } }
25.  }

27.  output { 
28.      elasticsearch {
29.      	hosts => ["https://192.168.0.3:9200"]
30.      	index => "bbc-news-elser"
31.      	api_key => "G4WdCY4Bu85zg5-6pKne:RIj_XbEbREuDySzRxYbkQA"
32.  		ssl_verification_mode => "full"
33.  		ssl_truststore_path => "/Users/liuxg/elastic/elasticsearch-8.12.0/config/certs/truststore.p12"
34.  		ssl_truststore_password => "password"
35.          pipeline => "news-pipeline"
36.          document_id => "%{[@metadata][fingerprint]}"
37.    	}

39.      stdout { codec => rubydebug }
40.  }

我在 Logstash 的安装目录中,使用如下的命令:

bash 复制代码
./bin/logstash -f logstash.conf

第 6 步:验证数据摄取

如果一切顺利,我们现在应该能够在集群中探索 BBC 新闻数据。

使用 pubDate 字段作为 "Timestamp field" 在 Discover 或 Stack Management 中创建数据视图。

我们可以通过如下的命令来查看是否已经完全写入:

如果完全写入,Logstash 的 termninal 将不再滚动。我们可以在 Kibana 中进行查看:

为了更好地了解 NER 模型的内部情况,我们可以在开发工具中查询数据,定制响应以返回感兴趣的字段:

arduino 复制代码
1.  GET bbc-news-elser/_search?size=1
2.  {
3.    "_source": ["ml.ner", "title"],
4.    "fields": [
5.      "ml.ner", "title"
6.    ]
7.  }

分解这个片段,我们可以看到原始的 "title" 值,以及 NER 模型产生的结果。 "predicted_value:" 字段显示带有注释的识别实体的文本。 在本案中,"putin" 和 "tucker carlson" 已被识别为人员 (PER),而 "fox" 被识别为一个组织。 "entities" 对象包含一个对象,每个对象代表在原始 "title" 字段中识别的命名实体,并包括:

  • "entity" - 文本中识别的命名实体的字符串。
  • "class_naAme" - 分配给实体的分类,即 PER、LOC、ORG。
  • "class_probability" - 表示模型对实体分类的置信度的十进制值。 上述响应中两个实体的值都接近 1,表明置信度较高。
  • "start_pos" 和 "end_pos" - 预测值文本中实体的开始和结束位置(零索引),这对于需要突出显示或进一步处理文本中特定实体的应用程序非常有用。

第 7 步:部署搜索 UI

在最后一步中,我们引入了 Streamlit 应用程序,该应用程序利用 BBC 新闻数据集进行语义和标准文本搜索。

首先,按照此处所述的步骤安装 Streamlit:Streamlit Git Repo,或使用位于 git 存储库中的 requirements.text 文件。 安装后,创建一个名为 elasticapp.py 的文件并添加 Python 代码块。 如上,当我们需要对云集群进行身份验证时,需要在运行之前设置 "CLOUD_ID"、"API_KEY" 变量(或者,可以使用用户和密码来验证对集群的访问)。这可以通过以下方式实现 创建 dotenv 文件,或者通过导出变量。对于后一种方法,请运行以下命令:

ini 复制代码
1.  export CLOUD_ID={{cloud_id}}
2.  export API_KEY={{api_key}}

我们正在实现的用户界面有助于输入语义和标准查询、选择 Elasticsearch 索引以及随后启动对我们的文章数据集的搜索。 Elasticsearch 连接是使用从环境变量加载的云凭据建立的。 后端逻辑包括根据用户查询获取数据以及使用搜索结果更新 Streamlit 应用程序显示的功能。

elasticapp.py

python 复制代码
1.  import streamlit as st
2.  from elasticsearch import Elasticsearch
3.  import os
4.  from datetime import datetime

6.  cloud_id = os.getenv("CLOUD_ID")
7.  api_key = os.getenv("API_KEY")

9.  es = Elasticsearch(
10.      cloud_id=cloud_id,
11.      api_key=api_key
12.  )

14.  def main():
15.      st.title("Elasticsearch News App")

17.      selected_index = st.sidebar.selectbox("Elasticsearch Index", ["bbc-news-elser"], key="selected_index")

19.      if 'selected_tags' not in st.session_state:
20.          st.session_state['selected_tags'] = {"LOC": set(), "PER": set(), "MISC": set()}

22.      if 'search_results' not in st.session_state:
23.          st.session_state['search_results'] = fetch_recent_data(selected_index, size=20)

25.      semantic_query = st.text_input("Semantic Query:", key="semantic_query")
26.      regular_query = st.text_input("Standard Query:", key="regular_query")

28.      min_date, max_date = get_date_range(selected_index)
29.      start_date = st.date_input("Start Date", min_date, key="start_date")
30.      end_date = st.date_input("End Date", max_date, key="end_date")

32.      if st.button("Search"):
33.          st.session_state['search_results'] = fetch_data(selected_index, semantic_query, regular_query, start_date, end_date)
34.          st.session_state['selected_tags'] = {tag_type: set() for tag_type in ["LOC", "PER", "MISC"]}  # Reset filters on new search

36.      for tag_type in ["LOC", "PER", "MISC"]:
37.          current_tags = get_unique_tags(tag_type, st.session_state['search_results'])
38.          st.session_state['selected_tags'][tag_type] = st.sidebar.multiselect(f"Filter by {tag_type}", current_tags, key=f"filter_{tag_type}")

40.      filtered_results = filter_results_by_tags(st.session_state['search_results'], st.session_state['selected_tags'])
41.      update_results(filtered_results)

43.  def fetch_recent_data(index_name, size=100):
44.      try:
45.          query_body = {
46.              "size": size,
47.              "sort": [
48.                  {"pubDate": {"order": "desc"}},  # Primary sort by date
49.              ]
50.          }
51.          response = es.search(index=index_name, body=query_body)
52.          return [hit['_source'] for hit in response['hits']['hits']]
53.      except Exception as e:
54.          st.error(f"Error fetching recent data from Elasticsearch: {e}")
55.          return []

57.  # Helper function to calculate the earliest and latest dates in the index
58.  def get_date_range(index_name):
59.      max_date_aggregation = {
60.          "max_date": {
61.              "max": {
62.                  "field": "pubDate"
63.              }
64.          }
65.      }

67.      min_date_aggregation = {
68.          "min_date": {
69.              "min": {
70.                  "field": "pubDate"
71.              }
72.          }
73.      }

75.      max_date_result = es.search(index=index_name, body={"aggs": max_date_aggregation})
76.      min_date_result = es.search(index=index_name, body={"aggs": min_date_aggregation})

78.      max_date_bucket = max_date_result['aggregations']['max_date']
79.      min_date_bucket = min_date_result['aggregations']['min_date']

81.      max_date = max_date_bucket['value_as_string']
82.      min_date = min_date_bucket['value_as_string']

84.      if max_date:
85.          max_date = datetime.strptime(max_date, "%a, %d %b %Y %H:%M:%S GMT")
86.      else:
87.          max_date = datetime.today().date()

89.      if min_date:
90.          min_date = datetime.strptime(min_date, "%a, %d %b %Y %H:%M:%S GMT")
91.      else:
92.          min_date = datetime.today().date()

94.      return min_date, max_date

96.  # Updates results based on search
97.  def update_results(results):
98.      try:
99.          for result_item in results:
100.              # Display document titles as links
101.              title_with_link = f"[{result_item['title']}]({result_item['url']})"
102.              st.markdown(f"### {title_with_link}")

104.              st.write(result_item['description'])

106.              # Display timestamp with results
107.              timestamp = result_item.get('pubDate', '')
108.              if timestamp:
109.                  st.write(f"Published: {timestamp}")

111.              # Adds tags for entities
112.              tags = result_item.get('tags', {})
113.              if tags:
114.                  for tag_type, tag_values in tags.items():
115.                      for tag_value in tag_values:
116.                          # Define colors for extracted entity tags
117.                          tag_color = {
118.                              "LOC": "#3498db",
119.                              "PER": "#2ecc71",
120.                              "MISC": "#e74c3c"
121.                          }.get(tag_type, "#555555")

123.                          st.markdown(
124.                              f"<span style='background-color: {tag_color}; color: white; padding: 5px; margin: 2px; border-radius: 5px;'>{tag_type}: {tag_value}</span>",
125.                              unsafe_allow_html=True)

127.              st.write("---")

129.      except Exception as e:
130.          st.error(f"Error performing search in Elasticsearch: {e}")

132.  # Fetch data from ES based on index + queries. Specify size - can be modified.
133.  def fetch_data(index_name, semantic_query, regular_query, start_date=None, end_date=None, size=100):
134.      try:
135.          query_body = {
136.              "size": size,
137.              "query": {
138.                  "bool": {
139.                      "should": []
140.                  }
141.              }
142.          }

144.          # Add semantic query if provided by the user
145.          if semantic_query:
146.              query_body["query"]["bool"]["should"].append(
147.                  {"bool": {
148.                      "should": {
149.                          "text_expansion": {
150.                              "ml-elser-title.tokens": {
151.                                  "model_text": semantic_query,
152.                                  "model_id": ".elser_model_2",
153.                                  "boost": 9
154.                              }
155.                          },

157.                          "text_expansion": {
158.                              "ml-elser-description.tokens": {
159.                                  "model_text": semantic_query,
160.                                  "model_id": ".elser_model_2",
161.                                  "boost": 9
162.                              }
163.                          }
164.                      }
165.                  }}
166.              )

168.          # Add regular query if provided by the user
169.          if regular_query:
170.              query_body["query"]["bool"]["should"].append({
171.                  "query_string": {
172.                      "query": regular_query,
173.                      "boost": 8
174.                  }
175.              })

177.          # Add date range if provided
178.          if start_date or end_date:
179.              date_range_query = {
180.                  "range": {
181.                      "pubDate": {}
182.                  }
183.              }

185.              if start_date:
186.                  date_range_query["range"]["pubDate"]["gte"] = start_date.strftime("%a, %d %b %Y %H:%M:%S GMT")

188.              if end_date:
189.                  date_range_query["range"]["pubDate"]["lte"] = end_date.strftime("%a, %d %b %Y %H:%M:%S GMT")

191.              query_body["query"]["bool"]["must"] = date_range_query

193.          result = es.search(
194.              index=index_name,
195.              body=query_body
196.          )

198.          hits = result['hits']['hits']
199.          data = [{'_id': hit['_id'], 'title': hit['_source'].get('title', ''),
200.                   'description': hit['_source'].get('description', ''),
201.                   'tags': hit['_source'].get('tags', {}), 'pubDate': hit['_source'].get('pubDate', ''),
202.                   'url': hit['_source'].get('url', '')} for hit in hits]
203.          return data
204.      except Exception as e:
205.          st.error(f"Error fetching data from Elasticsearch: {e}")
206.          return []

208.  # Function to get unique tags of a specific type
209.  def get_unique_tags(tag_type, results):
210.      unique_tags = set()
211.      for result_item in results:
212.          tags = result_item.get('tags', {}).get(tag_type, [])
213.          unique_tags.update(tags)
214.      return sorted(unique_tags)

216.  # Function to filter results based on selected tags
217.  def filter_results_by_tags(results, selected_tags):
218.      filtered_results = []
219.      for result_item in results:
220.          tags = result_item.get('tags', {})
221.          add_result = True
222.          for tag_type, selected_values in selected_tags.items():
223.              if selected_values:
224.                  result_values = tags.get(tag_type, [])
225.                  if not any(value in selected_values for value in result_values):
226.                      add_result = False
227.                      break
228.          if add_result:
229.              filtered_results.append(result_item)
230.      return filtered_results

232.  if __name__ == "__main__":
233.      main()

针对我们的本地部署来说,我们需要做如下的修改。我们可以参照之前的文章 "Elasticsearch:与多个 PDF 聊天 | LangChain Python 应用教程(免费 LLMs 和嵌入)"。在运行之前,我们先配置如下的环境变量:

ini 复制代码
1.  export ES_SERVER="localhost"
2.  export ES_USER="elastic"
3.  export ES_PASSWORD="q2rqAIphl-fx9ndQ36CO"
4.  export ES_FINGERPRINT="bce66ed55097f255fc8e4420bdadafc8d609cc8027038c2dd09d805668f3459e"
ini 复制代码
1.  $ export ES_SERVR="localhost"
2.  $ export ES_USER="elastic"
3.  $ export ES_PASSWORD="q2rqAIphl-fx9ndQ36CO"
4.  $ export ES_FINGERPRINT="bce66ed55097f255fc8e4420bdadafc8d609cc8027038c2dd09d805668f3459e"

elasticapp.py

python 复制代码
1.  import streamlit as st
2.  from elasticsearch import Elasticsearch
3.  import os
4.  from datetime import datetime

6.  endpoint = os.getenv("ES_SERVER")
7.  username = os.getenv("ES_USER")
8.  password = os.getenv("ES_PASSWORD")
9.  fingerprint = os.getenv("ES_FINGERPRINT")

11.  url = f"https://{endpoint}:9200"

13.  es = Elasticsearch( url ,
14.      basic_auth = (username, password),
15.      ssl_assert_fingerprint = fingerprint,
16.      http_compress = True )

18.  # print(es.info())

20.  def main():
21.      st.title("Elasticsearch News App")

23.      selected_index = st.sidebar.selectbox("Elasticsearch Index", ["bbc-news-elser"], key="selected_index")

25.      if 'selected_tags' not in st.session_state:
26.          st.session_state['selected_tags'] = {"LOC": set(), "PER": set(), "MISC": set()}

28.      if 'search_results' not in st.session_state:
29.          st.session_state['search_results'] = fetch_recent_data(selected_index, size=20)

31.      semantic_query = st.text_input("Semantic Query:", key="semantic_query")
32.      regular_query = st.text_input("Standard Query:", key="regular_query")

34.      min_date, max_date = get_date_range(selected_index)
35.      start_date = st.date_input("Start Date", min_date, key="start_date")
36.      end_date = st.date_input("End Date", max_date, key="end_date")

38.      if st.button("Search"):
39.          st.session_state['search_results'] = fetch_data(selected_index, semantic_query, regular_query, start_date, end_date)
40.          st.session_state['selected_tags'] = {tag_type: set() for tag_type in ["LOC", "PER", "MISC"]}  # Reset filters on new search

42.      for tag_type in ["LOC", "PER", "MISC"]:
43.          current_tags = get_unique_tags(tag_type, st.session_state['search_results'])
44.          st.session_state['selected_tags'][tag_type] = st.sidebar.multiselect(f"Filter by {tag_type}", current_tags, key=f"filter_{tag_type}")

46.      filtered_results = filter_results_by_tags(st.session_state['search_results'], st.session_state['selected_tags'])
47.      update_results(filtered_results)

49.  def fetch_recent_data(index_name, size=100):
50.      try:
51.          query_body = {
52.              "size": size,
53.              "sort": [
54.                  {"pubDate": {"order": "desc"}},  # Primary sort by date
55.              ]
56.          }
57.          response = es.search(index=index_name, body=query_body)
58.          return [hit['_source'] for hit in response['hits']['hits']]
59.      except Exception as e:
60.          st.error(f"Error fetching recent data from Elasticsearch: {e}")
61.          return []

63.  # Helper function to calculate the earliest and latest dates in the index
64.  def get_date_range(index_name):
65.      max_date_aggregation = {
66.          "max_date": {
67.              "max": {
68.                  "field": "pubDate"
69.              }
70.          }
71.      }

73.      min_date_aggregation = {
74.          "min_date": {
75.              "min": {
76.                  "field": "pubDate"
77.              }
78.          }
79.      }

81.      max_date_result = es.search(index=index_name, body={"aggs": max_date_aggregation})
82.      min_date_result = es.search(index=index_name, body={"aggs": min_date_aggregation})

84.      max_date_bucket = max_date_result['aggregations']['max_date']
85.      min_date_bucket = min_date_result['aggregations']['min_date']

87.      max_date = max_date_bucket['value_as_string']
88.      min_date = min_date_bucket['value_as_string']

90.      if max_date:
91.          max_date = datetime.strptime(max_date, "%a, %d %b %Y %H:%M:%S GMT")
92.      else:
93.          max_date = datetime.today().date()

95.      if min_date:
96.          min_date = datetime.strptime(min_date, "%a, %d %b %Y %H:%M:%S GMT")
97.      else:
98.          min_date = datetime.today().date()

100.      return min_date, max_date

102.  # Updates results based on search
103.  def update_results(results):
104.      try:
105.          for result_item in results:
106.              # Display document titles as links
107.              title_with_link = f"[{result_item['title']}]({result_item['url']})"
108.              st.markdown(f"### {title_with_link}")

110.              st.write(result_item['description'])

112.              # Display timestamp with results
113.              timestamp = result_item.get('pubDate', '')
114.              if timestamp:
115.                  st.write(f"Published: {timestamp}")

117.              # Adds tags for entities
118.              tags = result_item.get('tags', {})
119.              if tags:
120.                  for tag_type, tag_values in tags.items():
121.                      for tag_value in tag_values:
122.                          # Define colors for extracted entity tags
123.                          tag_color = {
124.                              "LOC": "#3498db",
125.                              "PER": "#2ecc71",
126.                              "MISC": "#e74c3c"
127.                          }.get(tag_type, "#555555")

129.                          st.markdown(
130.                              f"<span style='background-color: {tag_color}; color: white; padding: 5px; margin: 2px; border-radius: 5px;'>{tag_type}: {tag_value}</span>",
131.                              unsafe_allow_html=True)

133.              st.write("---")

135.      except Exception as e:
136.          st.error(f"Error performing search in Elasticsearch: {e}")

138.  # Fetch data from ES based on index + queries. Specify size - can be modified.
139.  def fetch_data(index_name, semantic_query, regular_query, start_date=None, end_date=None, size=100):
140.      try:
141.          query_body = {
142.              "size": size,
143.              "query": {
144.                  "bool": {
145.                      "should": []
146.                  }
147.              }
148.          }

150.          # Add semantic query if provided by the user
151.          if semantic_query:
152.              query_body["query"]["bool"]["should"].append(
153.                  {"bool": {
154.                      "should": {
155.                          "text_expansion": {
156.                              "ml-elser-title.tokens": {
157.                                  "model_text": semantic_query,
158.                                  "model_id": ".elser_model_2",
159.                                  "boost": 9
160.                              }
161.                          },

163.                          "text_expansion": {
164.                              "ml-elser-description.tokens": {
165.                                  "model_text": semantic_query,
166.                                  "model_id": ".elser_model_2",
167.                                  "boost": 9
168.                              }
169.                          }
170.                      }
171.                  }}
172.              )

174.          # Add regular query if provided by the user
175.          if regular_query:
176.              query_body["query"]["bool"]["should"].append({
177.                  "query_string": {
178.                      "query": regular_query,
179.                      "boost": 8
180.                  }
181.              })

183.          # Add date range if provided
184.          if start_date or end_date:
185.              date_range_query = {
186.                  "range": {
187.                      "pubDate": {}
188.                  }
189.              }

191.              if start_date:
192.                  date_range_query["range"]["pubDate"]["gte"] = start_date.strftime("%a, %d %b %Y %H:%M:%S GMT")

194.              if end_date:
195.                  date_range_query["range"]["pubDate"]["lte"] = end_date.strftime("%a, %d %b %Y %H:%M:%S GMT")

197.              query_body["query"]["bool"]["must"] = date_range_query

199.          result = es.search(
200.              index=index_name,
201.              body=query_body
202.          )

204.          hits = result['hits']['hits']
205.          data = [{'_id': hit['_id'], 'title': hit['_source'].get('title', ''),
206.                   'description': hit['_source'].get('description', ''),
207.                   'tags': hit['_source'].get('tags', {}), 'pubDate': hit['_source'].get('pubDate', ''),
208.                   'url': hit['_source'].get('url', '')} for hit in hits]
209.          return data
210.      except Exception as e:
211.          st.error(f"Error fetching data from Elasticsearch: {e}")
212.          return []

214.  # Function to get unique tags of a specific type
215.  def get_unique_tags(tag_type, results):
216.      unique_tags = set()
217.      for result_item in results:
218.          tags = result_item.get('tags', {}).get(tag_type, [])
219.          unique_tags.update(tags)
220.      return sorted(unique_tags)

222.  # Function to filter results based on selected tags
223.  def filter_results_by_tags(results, selected_tags):
224.      filtered_results = []
225.      for result_item in results:
226.          tags = result_item.get('tags', {})
227.          add_result = True
228.          for tag_type, selected_values in selected_tags.items():
229.              if selected_values:
230.                  result_values = tags.get(tag_type, [])
231.                  if not any(value in selected_values for value in result_values):
232.                      add_result = False
233.                      break
234.          if add_result:
235.              filtered_results.append(result_item)
236.      return filtered_results

238.  if __name__ == "__main__":
239.      main()

我们使用如下的命令来进行运行:

arduino 复制代码
streamlit run elasticapp.py

在上面,我们可以通过地名,人名或 MISC 来进行筛选。

我们可以针对一些查询来进行语义搜索,比如如:

原文:Developing an Elastic Search App with Streamlit, Semantic Search, and Named Entity Extraction --- Elastic Search Labs

相关推荐
老友@3 小时前
Elasticsearch 全面解析
大数据·elasticsearch·搜索引擎
惜鸟5 小时前
Elasticsearch文档标签检索方案设计
后端·elasticsearch
程序辕日记1 天前
使用SQL查询ES数据
sql·elasticsearch·jenkins
啥都不懂的小小白1 天前
Elasticsearch入门指南(三) 之 高级篇
大数据·elasticsearch·jenkins
qq_5470261791 天前
Elasticsearch 集群搭建
大数据·elasticsearch·搜索引擎
destinyol1 天前
wsl-docker环境下启动ES报错vm.max_map_count [65530] is too low
elasticsearch·docker·容器
大哥喝阔落1 天前
git操作0409
大数据·git·elasticsearch
Elastic 中国社区官方博客2 天前
将 CrewAI 与 Elasticsearch 结合使用
大数据·人工智能·elasticsearch·机器学习·搜索引擎·ai·全文检索
铭毅天下2 天前
Elasticsearch 8.X 如何利用嵌入向量提升搜索能力?
大数据·elasticsearch·搜索引擎·全文检索
八股文领域大手子2 天前
《从 MyBatis-Plus 到 Elasticsearch:一个后端的性能优化踩坑实录》
elasticsearch·性能优化·mybatis