You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

search.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. #
  2. # Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. import logging
  17. import re
  18. import math
  19. from collections import OrderedDict
  20. from dataclasses import dataclass
  21. from rag.settings import TAG_FLD, PAGERANK_FLD
  22. from rag.utils import rmSpace, get_float
  23. from rag.nlp import rag_tokenizer, query
  24. import numpy as np
  25. from rag.utils.doc_store_conn import DocStoreConnection, MatchDenseExpr, FusionExpr, OrderByExpr
  26. def index_name(uid): return f"ragflow_{uid}"
  27. class Dealer:
  28. def __init__(self, dataStore: DocStoreConnection):
  29. self.qryr = query.FulltextQueryer()
  30. self.dataStore = dataStore
  31. @dataclass
  32. class SearchResult:
  33. total: int
  34. ids: list[str]
  35. query_vector: list[float] | None = None
  36. field: dict | None = None
  37. highlight: dict | None = None
  38. aggregation: list | dict | None = None
  39. keywords: list[str] | None = None
  40. group_docs: list[list] | None = None
  41. def get_vector(self, txt, emb_mdl, topk=10, similarity=0.1):
  42. qv, _ = emb_mdl.encode_queries(txt)
  43. shape = np.array(qv).shape
  44. if len(shape) > 1:
  45. raise Exception(
  46. f"Dealer.get_vector returned array's shape {shape} doesn't match expectation(exact one dimension).")
  47. embedding_data = [get_float(v) for v in qv]
  48. vector_column_name = f"q_{len(embedding_data)}_vec"
  49. return MatchDenseExpr(vector_column_name, embedding_data, 'float', 'cosine', topk, {"similarity": similarity})
  50. def get_filters(self, req):
  51. condition = dict()
  52. for key, field in {"kb_ids": "kb_id", "doc_ids": "doc_id"}.items():
  53. if key in req and req[key] is not None:
  54. condition[field] = req[key]
  55. # TODO(yzc): `available_int` is nullable however infinity doesn't support nullable columns.
  56. for key in ["knowledge_graph_kwd", "available_int", "entity_kwd", "from_entity_kwd", "to_entity_kwd", "removed_kwd"]:
  57. if key in req and req[key] is not None:
  58. condition[key] = req[key]
  59. return condition
  60. def search(self, req, idx_names: str | list[str],
  61. kb_ids: list[str],
  62. emb_mdl=None,
  63. highlight=False,
  64. rank_feature: dict | None = None
  65. ):
  66. filters = self.get_filters(req)
  67. orderBy = OrderByExpr()
  68. pg = int(req.get("page", 1)) - 1
  69. topk = int(req.get("topk", 1024))
  70. ps = int(req.get("size", topk))
  71. offset, limit = pg * ps, ps
  72. src = req.get("fields",
  73. ["docnm_kwd", "content_ltks", "kb_id", "img_id", "title_tks", "important_kwd", "position_int",
  74. "doc_id", "page_num_int", "top_int", "create_timestamp_flt", "knowledge_graph_kwd",
  75. "question_kwd", "question_tks", "doc_type_kwd",
  76. "available_int", "content_with_weight", PAGERANK_FLD, TAG_FLD])
  77. kwds = set([])
  78. qst = req.get("question", "")
  79. q_vec = []
  80. if not qst:
  81. if req.get("sort"):
  82. orderBy.asc("page_num_int")
  83. orderBy.asc("top_int")
  84. orderBy.desc("create_timestamp_flt")
  85. res = self.dataStore.search(src, [], filters, [], orderBy, offset, limit, idx_names, kb_ids)
  86. total = self.dataStore.getTotal(res)
  87. logging.debug("Dealer.search TOTAL: {}".format(total))
  88. else:
  89. highlightFields = ["content_ltks", "title_tks"] if highlight else []
  90. matchText, keywords = self.qryr.question(qst, min_match=0.3)
  91. if emb_mdl is None:
  92. matchExprs = [matchText]
  93. res = self.dataStore.search(src, highlightFields, filters, matchExprs, orderBy, offset, limit,
  94. idx_names, kb_ids, rank_feature=rank_feature)
  95. total = self.dataStore.getTotal(res)
  96. logging.debug("Dealer.search TOTAL: {}".format(total))
  97. else:
  98. matchDense = self.get_vector(qst, emb_mdl, topk, req.get("similarity", 0.1))
  99. q_vec = matchDense.embedding_data
  100. src.append(f"q_{len(q_vec)}_vec")
  101. fusionExpr = FusionExpr("weighted_sum", topk, {"weights": "0.05,0.95"})
  102. matchExprs = [matchText, matchDense, fusionExpr]
  103. res = self.dataStore.search(src, highlightFields, filters, matchExprs, orderBy, offset, limit,
  104. idx_names, kb_ids, rank_feature=rank_feature)
  105. total = self.dataStore.getTotal(res)
  106. logging.debug("Dealer.search TOTAL: {}".format(total))
  107. # If result is empty, try again with lower min_match
  108. if total == 0:
  109. if filters.get("doc_id"):
  110. res = self.dataStore.search(src, [], filters, [], orderBy, offset, limit, idx_names, kb_ids)
  111. total = self.dataStore.getTotal(res)
  112. else:
  113. matchText, _ = self.qryr.question(qst, min_match=0.1)
  114. matchDense.extra_options["similarity"] = 0.17
  115. res = self.dataStore.search(src, highlightFields, filters, [matchText, matchDense, fusionExpr],
  116. orderBy, offset, limit, idx_names, kb_ids, rank_feature=rank_feature)
  117. total = self.dataStore.getTotal(res)
  118. logging.debug("Dealer.search 2 TOTAL: {}".format(total))
  119. for k in keywords:
  120. kwds.add(k)
  121. for kk in rag_tokenizer.fine_grained_tokenize(k).split():
  122. if len(kk) < 2:
  123. continue
  124. if kk in kwds:
  125. continue
  126. kwds.add(kk)
  127. logging.debug(f"TOTAL: {total}")
  128. ids = self.dataStore.getChunkIds(res)
  129. keywords = list(kwds)
  130. highlight = self.dataStore.getHighlight(res, keywords, "content_with_weight")
  131. aggs = self.dataStore.getAggregation(res, "docnm_kwd")
  132. return self.SearchResult(
  133. total=total,
  134. ids=ids,
  135. query_vector=q_vec,
  136. aggregation=aggs,
  137. highlight=highlight,
  138. field=self.dataStore.getFields(res, src),
  139. keywords=keywords
  140. )
  141. @staticmethod
  142. def trans2floats(txt):
  143. return [get_float(t) for t in txt.split("\t")]
  144. def insert_citations(self, answer, chunks, chunk_v,
  145. embd_mdl, tkweight=0.1, vtweight=0.9):
  146. assert len(chunks) == len(chunk_v)
  147. if not chunks:
  148. return answer, set([])
  149. pieces = re.split(r"(```)", answer)
  150. if len(pieces) >= 3:
  151. i = 0
  152. pieces_ = []
  153. while i < len(pieces):
  154. if pieces[i] == "```":
  155. st = i
  156. i += 1
  157. while i < len(pieces) and pieces[i] != "```":
  158. i += 1
  159. if i < len(pieces):
  160. i += 1
  161. pieces_.append("".join(pieces[st: i]) + "\n")
  162. else:
  163. pieces_.extend(
  164. re.split(
  165. r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])",
  166. pieces[i]))
  167. i += 1
  168. pieces = pieces_
  169. else:
  170. pieces = re.split(r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])", answer)
  171. for i in range(1, len(pieces)):
  172. if re.match(r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])", pieces[i]):
  173. pieces[i - 1] += pieces[i][0]
  174. pieces[i] = pieces[i][1:]
  175. idx = []
  176. pieces_ = []
  177. for i, t in enumerate(pieces):
  178. if len(t) < 5:
  179. continue
  180. idx.append(i)
  181. pieces_.append(t)
  182. logging.debug("{} => {}".format(answer, pieces_))
  183. if not pieces_:
  184. return answer, set([])
  185. ans_v, _ = embd_mdl.encode(pieces_)
  186. for i in range(len(chunk_v)):
  187. if len(ans_v[0]) != len(chunk_v[i]):
  188. chunk_v[i] = [0.0]*len(ans_v[0])
  189. logging.warning("The dimension of query and chunk do not match: {} vs. {}".format(len(ans_v[0]), len(chunk_v[i])))
  190. assert len(ans_v[0]) == len(chunk_v[0]), "The dimension of query and chunk do not match: {} vs. {}".format(
  191. len(ans_v[0]), len(chunk_v[0]))
  192. chunks_tks = [rag_tokenizer.tokenize(self.qryr.rmWWW(ck)).split()
  193. for ck in chunks]
  194. cites = {}
  195. thr = 0.63
  196. while thr > 0.3 and len(cites.keys()) == 0 and pieces_ and chunks_tks:
  197. for i, a in enumerate(pieces_):
  198. sim, tksim, vtsim = self.qryr.hybrid_similarity(ans_v[i],
  199. chunk_v,
  200. rag_tokenizer.tokenize(
  201. self.qryr.rmWWW(pieces_[i])).split(),
  202. chunks_tks,
  203. tkweight, vtweight)
  204. mx = np.max(sim) * 0.99
  205. logging.debug("{} SIM: {}".format(pieces_[i], mx))
  206. if mx < thr:
  207. continue
  208. cites[idx[i]] = list(
  209. set([str(ii) for ii in range(len(chunk_v)) if sim[ii] > mx]))[:4]
  210. thr *= 0.8
  211. res = ""
  212. seted = set([])
  213. for i, p in enumerate(pieces):
  214. res += p
  215. if i not in idx:
  216. continue
  217. if i not in cites:
  218. continue
  219. for c in cites[i]:
  220. assert int(c) < len(chunk_v)
  221. for c in cites[i]:
  222. if c in seted:
  223. continue
  224. res += f" [ID:{c}]"
  225. seted.add(c)
  226. return res, seted
  227. def _rank_feature_scores(self, query_rfea, search_res):
  228. ## For rank feature(tag_fea) scores.
  229. rank_fea = []
  230. pageranks = []
  231. for chunk_id in search_res.ids:
  232. pageranks.append(search_res.field[chunk_id].get(PAGERANK_FLD, 0))
  233. pageranks = np.array(pageranks, dtype=float)
  234. if not query_rfea:
  235. return np.array([0 for _ in range(len(search_res.ids))]) + pageranks
  236. q_denor = np.sqrt(np.sum([s*s for t,s in query_rfea.items() if t != PAGERANK_FLD]))
  237. for i in search_res.ids:
  238. nor, denor = 0, 0
  239. if not search_res.field[i].get(TAG_FLD):
  240. rank_fea.append(0)
  241. continue
  242. for t, sc in eval(search_res.field[i].get(TAG_FLD, "{}")).items():
  243. if t in query_rfea:
  244. nor += query_rfea[t] * sc
  245. denor += sc * sc
  246. if denor == 0:
  247. rank_fea.append(0)
  248. else:
  249. rank_fea.append(nor/np.sqrt(denor)/q_denor)
  250. return np.array(rank_fea)*10. + pageranks
  251. def rerank(self, sres, query, tkweight=0.3,
  252. vtweight=0.7, cfield="content_ltks",
  253. rank_feature: dict | None = None
  254. ):
  255. _, keywords = self.qryr.question(query)
  256. vector_size = len(sres.query_vector)
  257. vector_column = f"q_{vector_size}_vec"
  258. zero_vector = [0.0] * vector_size
  259. ins_embd = []
  260. for chunk_id in sres.ids:
  261. vector = sres.field[chunk_id].get(vector_column, zero_vector)
  262. if isinstance(vector, str):
  263. vector = [get_float(v) for v in vector.split("\t")]
  264. ins_embd.append(vector)
  265. if not ins_embd:
  266. return [], [], []
  267. for i in sres.ids:
  268. if isinstance(sres.field[i].get("important_kwd", []), str):
  269. sres.field[i]["important_kwd"] = [sres.field[i]["important_kwd"]]
  270. ins_tw = []
  271. for i in sres.ids:
  272. content_ltks = list(OrderedDict.fromkeys(sres.field[i][cfield].split()))
  273. title_tks = [t for t in sres.field[i].get("title_tks", "").split() if t]
  274. question_tks = [t for t in sres.field[i].get("question_tks", "").split() if t]
  275. important_kwd = sres.field[i].get("important_kwd", [])
  276. tks = content_ltks + title_tks * 2 + important_kwd * 5 + question_tks * 6
  277. ins_tw.append(tks)
  278. ## For rank feature(tag_fea) scores.
  279. rank_fea = self._rank_feature_scores(rank_feature, sres)
  280. sim, tksim, vtsim = self.qryr.hybrid_similarity(sres.query_vector,
  281. ins_embd,
  282. keywords,
  283. ins_tw, tkweight, vtweight)
  284. return sim + rank_fea, tksim, vtsim
  285. def rerank_by_model(self, rerank_mdl, sres, query, tkweight=0.3,
  286. vtweight=0.7, cfield="content_ltks",
  287. rank_feature: dict | None = None):
  288. _, keywords = self.qryr.question(query)
  289. for i in sres.ids:
  290. if isinstance(sres.field[i].get("important_kwd", []), str):
  291. sres.field[i]["important_kwd"] = [sres.field[i]["important_kwd"]]
  292. ins_tw = []
  293. for i in sres.ids:
  294. content_ltks = sres.field[i][cfield].split()
  295. title_tks = [t for t in sres.field[i].get("title_tks", "").split() if t]
  296. important_kwd = sres.field[i].get("important_kwd", [])
  297. tks = content_ltks + title_tks + important_kwd
  298. ins_tw.append(tks)
  299. tksim = self.qryr.token_similarity(keywords, ins_tw)
  300. vtsim, _ = rerank_mdl.similarity(query, [rmSpace(" ".join(tks)) for tks in ins_tw])
  301. ## For rank feature(tag_fea) scores.
  302. rank_fea = self._rank_feature_scores(rank_feature, sres)
  303. return tkweight * (np.array(tksim)+rank_fea) + vtweight * vtsim, tksim, vtsim
  304. def hybrid_similarity(self, ans_embd, ins_embd, ans, inst):
  305. return self.qryr.hybrid_similarity(ans_embd,
  306. ins_embd,
  307. rag_tokenizer.tokenize(ans).split(),
  308. rag_tokenizer.tokenize(inst).split())
  309. def retrieval(self, question, embd_mdl, tenant_ids, kb_ids, page, page_size, similarity_threshold=0.2,
  310. vector_similarity_weight=0.3, top=1024, doc_ids=None, aggs=True,
  311. rerank_mdl=None, highlight=False,
  312. rank_feature: dict | None = {PAGERANK_FLD: 10}):
  313. ranks = {"total": 0, "chunks": [], "doc_aggs": {}}
  314. if not question:
  315. return ranks
  316. RERANK_LIMIT = 64
  317. RERANK_LIMIT = int(RERANK_LIMIT//page_size + ((RERANK_LIMIT%page_size)/(page_size*1.) + 0.5)) * page_size if page_size>1 else 1
  318. if RERANK_LIMIT < 1: ## when page_size is very large the RERANK_LIMIT will be 0.
  319. RERANK_LIMIT = 1
  320. req = {"kb_ids": kb_ids, "doc_ids": doc_ids, "page": math.ceil(page_size*page/RERANK_LIMIT), "size": RERANK_LIMIT,
  321. "question": question, "vector": True, "topk": top,
  322. "similarity": similarity_threshold,
  323. "available_int": 1}
  324. if isinstance(tenant_ids, str):
  325. tenant_ids = tenant_ids.split(",")
  326. sres = self.search(req, [index_name(tid) for tid in tenant_ids],
  327. kb_ids, embd_mdl, highlight, rank_feature=rank_feature)
  328. if rerank_mdl and sres.total > 0:
  329. sim, tsim, vsim = self.rerank_by_model(rerank_mdl,
  330. sres, question, 1 - vector_similarity_weight,
  331. vector_similarity_weight,
  332. rank_feature=rank_feature)
  333. else:
  334. sim, tsim, vsim = self.rerank(
  335. sres, question, 1 - vector_similarity_weight, vector_similarity_weight,
  336. rank_feature=rank_feature)
  337. # Already paginated in search function
  338. idx = np.argsort(sim * -1)[(page - 1) * page_size:page * page_size]
  339. dim = len(sres.query_vector)
  340. vector_column = f"q_{dim}_vec"
  341. zero_vector = [0.0] * dim
  342. sim_np = np.array(sim)
  343. if doc_ids:
  344. similarity_threshold = 0
  345. filtered_count = (sim_np >= similarity_threshold).sum()
  346. ranks["total"] = int(filtered_count) # Convert from np.int64 to Python int otherwise JSON serializable error
  347. for i in idx:
  348. if sim[i] < similarity_threshold:
  349. break
  350. id = sres.ids[i]
  351. chunk = sres.field[id]
  352. dnm = chunk.get("docnm_kwd", "")
  353. did = chunk.get("doc_id", "")
  354. if len(ranks["chunks"]) >= page_size:
  355. if aggs:
  356. if dnm not in ranks["doc_aggs"]:
  357. ranks["doc_aggs"][dnm] = {"doc_id": did, "count": 0}
  358. ranks["doc_aggs"][dnm]["count"] += 1
  359. continue
  360. break
  361. position_int = chunk.get("position_int", [])
  362. d = {
  363. "chunk_id": id,
  364. "content_ltks": chunk["content_ltks"],
  365. "content_with_weight": chunk["content_with_weight"],
  366. "doc_id": did,
  367. "docnm_kwd": dnm,
  368. "kb_id": chunk["kb_id"],
  369. "important_kwd": chunk.get("important_kwd", []),
  370. "image_id": chunk.get("img_id", ""),
  371. "similarity": sim[i],
  372. "vector_similarity": vsim[i],
  373. "term_similarity": tsim[i],
  374. "vector": chunk.get(vector_column, zero_vector),
  375. "positions": position_int,
  376. "doc_type_kwd": chunk.get("doc_type_kwd", "")
  377. }
  378. if highlight and sres.highlight:
  379. if id in sres.highlight:
  380. d["highlight"] = rmSpace(sres.highlight[id])
  381. else:
  382. d["highlight"] = d["content_with_weight"]
  383. ranks["chunks"].append(d)
  384. if dnm not in ranks["doc_aggs"]:
  385. ranks["doc_aggs"][dnm] = {"doc_id": did, "count": 0}
  386. ranks["doc_aggs"][dnm]["count"] += 1
  387. ranks["doc_aggs"] = [{"doc_name": k,
  388. "doc_id": v["doc_id"],
  389. "count": v["count"]} for k,
  390. v in sorted(ranks["doc_aggs"].items(),
  391. key=lambda x: x[1]["count"] * -1)]
  392. ranks["chunks"] = ranks["chunks"][:page_size]
  393. return ranks
  394. def sql_retrieval(self, sql, fetch_size=128, format="json"):
  395. tbl = self.dataStore.sql(sql, fetch_size, format)
  396. return tbl
  397. def chunk_list(self, doc_id: str, tenant_id: str,
  398. kb_ids: list[str], max_count=1024,
  399. offset=0,
  400. fields=["docnm_kwd", "content_with_weight", "img_id"]):
  401. condition = {"doc_id": doc_id}
  402. res = []
  403. bs = 128
  404. for p in range(offset, max_count, bs):
  405. es_res = self.dataStore.search(fields, [], condition, [], OrderByExpr(), p, bs, index_name(tenant_id),
  406. kb_ids)
  407. dict_chunks = self.dataStore.getFields(es_res, fields)
  408. for id, doc in dict_chunks.items():
  409. doc["id"] = id
  410. if dict_chunks:
  411. res.extend(dict_chunks.values())
  412. if len(dict_chunks.values()) < bs:
  413. break
  414. return res
  415. def all_tags(self, tenant_id: str, kb_ids: list[str], S=1000):
  416. if not self.dataStore.indexExist(index_name(tenant_id), kb_ids[0]):
  417. return []
  418. res = self.dataStore.search([], [], {}, [], OrderByExpr(), 0, 0, index_name(tenant_id), kb_ids, ["tag_kwd"])
  419. return self.dataStore.getAggregation(res, "tag_kwd")
  420. def all_tags_in_portion(self, tenant_id: str, kb_ids: list[str], S=1000):
  421. res = self.dataStore.search([], [], {}, [], OrderByExpr(), 0, 0, index_name(tenant_id), kb_ids, ["tag_kwd"])
  422. res = self.dataStore.getAggregation(res, "tag_kwd")
  423. total = np.sum([c for _, c in res])
  424. return {t: (c + 1) / (total + S) for t, c in res}
  425. def tag_content(self, tenant_id: str, kb_ids: list[str], doc, all_tags, topn_tags=3, keywords_topn=30, S=1000):
  426. idx_nm = index_name(tenant_id)
  427. match_txt = self.qryr.paragraph(doc["title_tks"] + " " + doc["content_ltks"], doc.get("important_kwd", []), keywords_topn)
  428. res = self.dataStore.search([], [], {}, [match_txt], OrderByExpr(), 0, 0, idx_nm, kb_ids, ["tag_kwd"])
  429. aggs = self.dataStore.getAggregation(res, "tag_kwd")
  430. if not aggs:
  431. return False
  432. cnt = np.sum([c for _, c in aggs])
  433. tag_fea = sorted([(a, round(0.1*(c + 1) / (cnt + S) / max(1e-6, all_tags.get(a, 0.0001)))) for a, c in aggs],
  434. key=lambda x: x[1] * -1)[:topn_tags]
  435. doc[TAG_FLD] = {a.replace(".", "_"): c for a, c in tag_fea if c > 0}
  436. return True
  437. def tag_query(self, question: str, tenant_ids: str | list[str], kb_ids: list[str], all_tags, topn_tags=3, S=1000):
  438. if isinstance(tenant_ids, str):
  439. idx_nms = index_name(tenant_ids)
  440. else:
  441. idx_nms = [index_name(tid) for tid in tenant_ids]
  442. match_txt, _ = self.qryr.question(question, min_match=0.0)
  443. res = self.dataStore.search([], [], {}, [match_txt], OrderByExpr(), 0, 0, idx_nms, kb_ids, ["tag_kwd"])
  444. aggs = self.dataStore.getAggregation(res, "tag_kwd")
  445. if not aggs:
  446. return {}
  447. cnt = np.sum([c for _, c in aggs])
  448. tag_fea = sorted([(a, round(0.1*(c + 1) / (cnt + S) / max(1e-6, all_tags.get(a, 0.0001)))) for a, c in aggs],
  449. key=lambda x: x[1] * -1)[:topn_tags]
  450. return {a.replace(".", "_"): max(1, c) for a, c in tag_fea}