前言
在常规搜索中,我们通常依赖数据库的直接SELECT查询,对于文本内容,多数情况下使用LIKE进行模糊匹配,并通过添加判断规则来限制条件,以确保返回结果符合预期。然而,面对手中这批关于留学的问题和答案,当前的检索需求不仅要求考虑时效性(即信息易过期),而且单纯的模糊搜索已不足以满足需求。因此,我们决定研发一个更为先进的搜索模式,以便更好地维护数据并满足复杂的搜索需求。
1、尝试使用ES
ES是我听过最多做搜索的引擎,首先,使用全文检索引擎如Elasticsearch或Solr来处理大量文本数据,支持复杂查询,包括关键词搜索、模糊搜索、近似匹配和权重排序。其次,采用自然语言处理(NLP)技术提升搜索的智能化和准确性,包括分词与词干提取、同义词识别和实体识别。此外,我们将引入时效性处理机制,通过时间戳过滤和时间权重,确保数据的时效性。同时,通过分析用户的搜索和点击行为,利用点击率和停留时间来优化搜索结果排序。
- 创建索引结构
{
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "1",
"max_result_window": "10000",
"analysis": {
"filter": {
"remote_synonym": {
"type": "dynamic_synonym",
"synonyms_path": "http://192.168.0.138:1616/knowledge/getKnowledgeEsIkWorks?type=2",
"interval": 30
}
},
"char_filter": {
"html_strip": {
"type": "html_strip"
}
},
"analyzer": {
"ik_max_syno": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": [
"remote_synonym"
]
}
},
"tokenizer": {
"custom_tokenizer": {
"type": "standard"
}
}
}
}
},
}
mapping我就删掉了,可以自己加入对应表结构的mapping!针对不同的分析场景
也可以多写一点分析器,列如:对于客户场景,加入停用词场景!
- 完善分词器,加入IK远程词典同义词和停用词
分词器需要提前安装IK分词器和elasticsearch-analysis-dynamic-synonym
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://localhost:1616/knowledge/getKnowledgeEsIkWorks?type=1</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">http://localhost:1616/knowledge/getKnowledgeEsIkWorks?type=3</entry>
</properties>
其中两个词典的位置直接用服务端写一个请求就好,根据官网,请求头携带ETag和Last-Modified
这块分词器刷新词典的时间是一分钟一次,所以如果词典数据较多的话,记得添加缓存和通过这两个请求头来限制刷新时间
- 将mysql的数据洗进去
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.clusterNodes}")
private String clusterNodes;
@Value("${elasticsearch.clusterName}")
private String clusterName;
@Bean
public RestHighLevelClient restHighLevelClient() {
// 解析hostlist配置信息
String[] split = clusterNodes.split(",");
// 创建HttpHost数组,存放es主机和端口的配置信息
HttpHost[] httpHostArray = new HttpHost[split.length];
for(int i = 0; i < split.length; i++) {
String item = split[i];
httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
}
// 创建RestHighLevelClient客户端
RestClientBuilder builder = RestClient.builder(httpHostArray);
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
我是用java写的,其他语音也类似,直接build,对着mapping的字段拼成想要json串
- 尝试简单匹配搜索
{
"query":{
"match":{
"title":"阿大"
}
}
}
我这边是把多个字段拼成一个去搜索,这一步完成,已经和数据库的模糊查重差不多了!
- 增加条件和脚本
简单的完了,咋来一个不简单的
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "阿大",
"fields": [
"knowledge.class^0.35",
"knowledge.schoolId^0.3",
"title^0.2",
"content^0.15"
],
"type": "best_fields",
"operator": "or",
"analyzer": "ik_max_word"
}
}
}
},
"sort": {
"_script": {
"type": "number",
"script": {
"source": "def releaseDate = doc['releastime'].value.toInstant().toEpochMilli();def now = System.currentTimeMillis();def time1 = now - (30 * 24 * 60 * 60 * 1000);def time2 = now - (90 * 24 * 60 * 60 * 1000);def time3 = now - (180 * 24 * 60 * 60 * 1000);return _score * (releaseDate > time1 ? 1.2 : (releaseDate > time2 ? 1.1 : (releaseDate > time3 ? 1 : 0.4)));",
"params": {
"time1": "now-1M",
"time2": "now-3M",
"time3": "now-6M"
}
},
"order": "desc"
}
}
}
惊不惊喜,意不意外!!
对指定字段的评分乘积以及时间衰减做了简单的查询调整
这块ES7貌似是支持时间衰减的,目前没有具体的业务规则,就先用自己先的脚本试试了
- 到这个程度,通过维护了一段时间的停用词和同义词,以及分词的关键词,ES搜索就告一段落了,ES搜索还有很多强大的功能有待发掘,学无止境学无止境
2、尝试使用向量数据库
ES虽然对相似度,匹配度上已经有了提升,但是语义查询,还在更高版本才支持。
这边一边维护旧版本,一边也在找新的思路,就找到了Qdrant向量数据库
首先尝试的要求首先就是开源,所以我试了Qdrant和Weaviate
向量数据库和ES有异曲同工之处,也是创建索引,洗数据,查询这一套基本流程
区别就在于mysql数据的文本转向量的效果会直接影响到查询的效果,所以选择一个强大的模型就成了至关重要的事。
huggingface 开源模型
这个网站可以需要小飞机,而且容易下载断开,下载的时候去搜一下huggingface-cli
好评五分
我这边选的文本转向量的模型是bge-large-zn-v1.5,还有其他的图片转向量,人脸转向量,如果有其他的场景可以尝试其他的模型!其中向量大小也会影响查询效果,对于文本更多的数据选用更大的向量宽
看到这的记得给个赞!!哈哈哈(插一波广告)
from flask import Flask, request, jsonify
from sentence_transformers import SentenceTransformer
import numpy as np
app = Flask(__name__)
#model_path = 'D:\\project\\python_project\\models\\bert-base-uncased'
model_path = 'D:\\project\\python_project\\models\\bge-large-zn-v1.5'
model = SentenceTransformer(model_path)
@app.route('/encode', methods=['POST'])
def encode_text():
# 接收POST请求中的JSON数据
data = request.json
text = data['text']
vector_size = data['vector_size']
# 将文本数据转换为指定大小的向量
encoded_vector = model.encode([text], normalize_embeddings=True, convert_to_tensor=False)[0][:vector_size]
# 返回结果
return jsonify(encoded_vector.tolist())
if __name__ == '__main__':
app.run(debug=True)
通过phthon提供转向量的接口,完成洗数据的工作,简单查询
post /collections/knowledge_index/points/search
{
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
"top": 5
}
有了语义查询,查询的相关词更加准确了,最终也确定了Qdrant向量数据库
3、开始尝试ES和Qdrant混合
ES有较好的分词、停用词功能
Qdrant有较好的语义查询
①. 首先在洗数据方面就开始先通过ES分词拆开,再把重复词去掉,再转向量的方式,保证重复词占用的比重
public String analyzeText(String text) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost request = new HttpPost(ES_URL);
request.setHeader("Content-Type", "application/json");
// Create JSON payload
String jsonPayload = "{ \"analyzer\": \"ik_smart\", \"text\": \"" + text + "\" }";
request.setEntity(new StringEntity(jsonPayload, "UTF-8"));
// Execute the request and get the response
try (CloseableHttpResponse response = httpClient.execute(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
// Parse the JSON response
JsonNode tokensNode = OBJECT_MAPPER.readTree(jsonResponse).path("tokens");
// Extract unique tokens
Set<String> uniqueTokens = new HashSet<>();
if (tokensNode.isArray()) {
for (JsonNode tokenNode : tokensNode) {
uniqueTokens.add(tokenNode.path("token").asText());
}
}
String result = uniqueTokens.stream().collect(Collectors.joining(""));
// Print the unique tokens (you can also return or assert them as needed)
return result;
}
}
}
②. 查询的时候讲输入词也用同样的去重操作,转向量,查询
③. 最后通过payload做一些简单的数据过滤,完成最终的查询
总结:ES是一个强大的搜索引擎,我开发深入的程度还很低,也需要更深层次的学习,才能完成更加复杂的查询
向量数据库是一个好的解决方案,针对人脸识别、图片识别、文本匹配都有较好的解决方案,实际使用过程中也看查询效果,也看实际成本。
学无止境学无止境!!!
(可以评论一起学习,我会看)