Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

search.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. from dataclasses import dataclass
  19. from rag.utils import rmSpace
  20. from rag.nlp import rag_tokenizer, query
  21. import numpy as np
  22. from rag.utils.doc_store_conn import DocStoreConnection, MatchDenseExpr, FusionExpr, OrderByExpr
  23. def index_name(uid): return f"ragflow_{uid}"
  24. class Dealer:
  25. def __init__(self, dataStore: DocStoreConnection):
  26. self.qryr = query.FulltextQueryer()
  27. self.dataStore = dataStore
  28. @dataclass
  29. class SearchResult:
  30. total: int
  31. ids: list[str]
  32. query_vector: list[float] | None = None
  33. field: dict | None = None
  34. highlight: dict | None = None
  35. aggregation: list | dict | None = None
  36. keywords: list[str] | None = None
  37. group_docs: list[list] | None = None
  38. def get_vector(self, txt, emb_mdl, topk=10, similarity=0.1):
  39. qv, _ = emb_mdl.encode_queries(txt)
  40. shape = np.array(qv).shape
  41. if len(shape) > 1:
  42. raise Exception(f"Dealer.get_vector returned array's shape {shape} doesn't match expectation(exact one dimension).")
  43. embedding_data = [float(v) for v in qv]
  44. vector_column_name = f"q_{len(embedding_data)}_vec"
  45. return MatchDenseExpr(vector_column_name, embedding_data, 'float', 'cosine', topk, {"similarity": similarity})
  46. def get_filters(self, req):
  47. condition = dict()
  48. for key, field in {"kb_ids": "kb_id", "doc_ids": "doc_id"}.items():
  49. if key in req and req[key] is not None:
  50. condition[field] = req[key]
  51. # TODO(yzc): `available_int` is nullable however infinity doesn't support nullable columns.
  52. for key in ["knowledge_graph_kwd", "available_int"]:
  53. if key in req and req[key] is not None:
  54. condition[key] = req[key]
  55. return condition
  56. def search(self, req, idx_names: str | list[str], kb_ids: list[str], emb_mdl=None, highlight = False):
  57. filters = self.get_filters(req)
  58. orderBy = OrderByExpr()
  59. pg = int(req.get("page", 1)) - 1
  60. topk = int(req.get("topk", 1024))
  61. ps = int(req.get("size", topk))
  62. offset, limit = pg * ps, (pg + 1) * ps
  63. src = req.get("fields", ["docnm_kwd", "content_ltks", "kb_id", "img_id", "title_tks", "important_kwd",
  64. "doc_id", "page_num_int", "top_int", "create_timestamp_flt", "knowledge_graph_kwd", "question_kwd", "question_tks",
  65. "available_int", "content_with_weight", "pagerank_fea"])
  66. kwds = set([])
  67. qst = req.get("question", "")
  68. q_vec = []
  69. if not qst:
  70. if req.get("sort"):
  71. orderBy.asc("page_num_int")
  72. orderBy.asc("top_int")
  73. orderBy.desc("create_timestamp_flt")
  74. res = self.dataStore.search(src, [], filters, [], orderBy, offset, limit, idx_names, kb_ids)
  75. total=self.dataStore.getTotal(res)
  76. logging.debug("Dealer.search TOTAL: {}".format(total))
  77. else:
  78. highlightFields = ["content_ltks", "title_tks"] if highlight else []
  79. matchText, keywords = self.qryr.question(qst, min_match=0.3)
  80. if emb_mdl is None:
  81. matchExprs = [matchText]
  82. res = self.dataStore.search(src, highlightFields, filters, matchExprs, orderBy, offset, limit, idx_names, kb_ids)
  83. total=self.dataStore.getTotal(res)
  84. logging.debug("Dealer.search TOTAL: {}".format(total))
  85. else:
  86. matchDense = self.get_vector(qst, emb_mdl, topk, req.get("similarity", 0.1))
  87. q_vec = matchDense.embedding_data
  88. src.append(f"q_{len(q_vec)}_vec")
  89. fusionExpr = FusionExpr("weighted_sum", topk, {"weights": "0.05, 0.95"})
  90. matchExprs = [matchText, matchDense, fusionExpr]
  91. res = self.dataStore.search(src, highlightFields, filters, matchExprs, orderBy, offset, limit, idx_names, kb_ids)
  92. total=self.dataStore.getTotal(res)
  93. logging.debug("Dealer.search TOTAL: {}".format(total))
  94. # If result is empty, try again with lower min_match
  95. if total == 0:
  96. matchText, _ = self.qryr.question(qst, min_match=0.1)
  97. filters.pop("doc_ids", None)
  98. matchDense.extra_options["similarity"] = 0.17
  99. res = self.dataStore.search(src, highlightFields, filters, [matchText, matchDense, fusionExpr], orderBy, offset, limit, idx_names, kb_ids)
  100. total=self.dataStore.getTotal(res)
  101. logging.debug("Dealer.search 2 TOTAL: {}".format(total))
  102. for k in keywords:
  103. kwds.add(k)
  104. for kk in rag_tokenizer.fine_grained_tokenize(k).split():
  105. if len(kk) < 2:
  106. continue
  107. if kk in kwds:
  108. continue
  109. kwds.add(kk)
  110. logging.debug(f"TOTAL: {total}")
  111. ids=self.dataStore.getChunkIds(res)
  112. keywords=list(kwds)
  113. highlight = self.dataStore.getHighlight(res, keywords, "content_with_weight")
  114. aggs = self.dataStore.getAggregation(res, "docnm_kwd")
  115. return self.SearchResult(
  116. total=total,
  117. ids=ids,
  118. query_vector=q_vec,
  119. aggregation=aggs,
  120. highlight=highlight,
  121. field=self.dataStore.getFields(res, src),
  122. keywords=keywords
  123. )
  124. @staticmethod
  125. def trans2floats(txt):
  126. return [float(t) for t in txt.split("\t")]
  127. def insert_citations(self, answer, chunks, chunk_v,
  128. embd_mdl, tkweight=0.1, vtweight=0.9):
  129. assert len(chunks) == len(chunk_v)
  130. if not chunks:
  131. return answer, set([])
  132. pieces = re.split(r"(```)", answer)
  133. if len(pieces) >= 3:
  134. i = 0
  135. pieces_ = []
  136. while i < len(pieces):
  137. if pieces[i] == "```":
  138. st = i
  139. i += 1
  140. while i < len(pieces) and pieces[i] != "```":
  141. i += 1
  142. if i < len(pieces):
  143. i += 1
  144. pieces_.append("".join(pieces[st: i]) + "\n")
  145. else:
  146. pieces_.extend(
  147. re.split(
  148. r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])",
  149. pieces[i]))
  150. i += 1
  151. pieces = pieces_
  152. else:
  153. pieces = re.split(r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])", answer)
  154. for i in range(1, len(pieces)):
  155. if re.match(r"([^\|][;。?!!\n]|[a-z][.?;!][ \n])", pieces[i]):
  156. pieces[i - 1] += pieces[i][0]
  157. pieces[i] = pieces[i][1:]
  158. idx = []
  159. pieces_ = []
  160. for i, t in enumerate(pieces):
  161. if len(t) < 5:
  162. continue
  163. idx.append(i)
  164. pieces_.append(t)
  165. logging.debug("{} => {}".format(answer, pieces_))
  166. if not pieces_:
  167. return answer, set([])
  168. ans_v, _ = embd_mdl.encode(pieces_)
  169. assert len(ans_v[0]) == len(chunk_v[0]), "The dimension of query and chunk do not match: {} vs. {}".format(
  170. len(ans_v[0]), len(chunk_v[0]))
  171. chunks_tks = [rag_tokenizer.tokenize(self.qryr.rmWWW(ck)).split()
  172. for ck in chunks]
  173. cites = {}
  174. thr = 0.63
  175. while thr>0.3 and len(cites.keys()) == 0 and pieces_ and chunks_tks:
  176. for i, a in enumerate(pieces_):
  177. sim, tksim, vtsim = self.qryr.hybrid_similarity(ans_v[i],
  178. chunk_v,
  179. rag_tokenizer.tokenize(
  180. self.qryr.rmWWW(pieces_[i])).split(),
  181. chunks_tks,
  182. tkweight, vtweight)
  183. mx = np.max(sim) * 0.99
  184. logging.debug("{} SIM: {}".format(pieces_[i], mx))
  185. if mx < thr:
  186. continue
  187. cites[idx[i]] = list(
  188. set([str(ii) for ii in range(len(chunk_v)) if sim[ii] > mx]))[:4]
  189. thr *= 0.8
  190. res = ""
  191. seted = set([])
  192. for i, p in enumerate(pieces):
  193. res += p
  194. if i not in idx:
  195. continue
  196. if i not in cites:
  197. continue
  198. for c in cites[i]:
  199. assert int(c) < len(chunk_v)
  200. for c in cites[i]:
  201. if c in seted:
  202. continue
  203. res += f" ##{c}$$"
  204. seted.add(c)
  205. return res, seted
  206. def rerank(self, sres, query, tkweight=0.3,
  207. vtweight=0.7, cfield="content_ltks"):
  208. _, keywords = self.qryr.question(query)
  209. vector_size = len(sres.query_vector)
  210. vector_column = f"q_{vector_size}_vec"
  211. zero_vector = [0.0] * vector_size
  212. ins_embd = []
  213. pageranks = []
  214. for chunk_id in sres.ids:
  215. vector = sres.field[chunk_id].get(vector_column, zero_vector)
  216. if isinstance(vector, str):
  217. vector = [float(v) for v in vector.split("\t")]
  218. ins_embd.append(vector)
  219. pageranks.append(sres.field[chunk_id].get("pagerank_fea", 0))
  220. if not ins_embd:
  221. return [], [], []
  222. for i in sres.ids:
  223. if isinstance(sres.field[i].get("important_kwd", []), str):
  224. sres.field[i]["important_kwd"] = [sres.field[i]["important_kwd"]]
  225. ins_tw = []
  226. for i in sres.ids:
  227. content_ltks = sres.field[i][cfield].split()
  228. title_tks = [t for t in sres.field[i].get("title_tks", "").split() if t]
  229. question_tks = [t for t in sres.field[i].get("question_tks", "").split() if t]
  230. important_kwd = sres.field[i].get("important_kwd", [])
  231. tks = content_ltks + title_tks*2 + important_kwd*5 + question_tks*6
  232. ins_tw.append(tks)
  233. sim, tksim, vtsim = self.qryr.hybrid_similarity(sres.query_vector,
  234. ins_embd,
  235. keywords,
  236. ins_tw, tkweight, vtweight)
  237. return sim+np.array(pageranks, dtype=float), tksim, vtsim
  238. def rerank_by_model(self, rerank_mdl, sres, query, tkweight=0.3,
  239. vtweight=0.7, cfield="content_ltks"):
  240. _, keywords = self.qryr.question(query)
  241. for i in sres.ids:
  242. if isinstance(sres.field[i].get("important_kwd", []), str):
  243. sres.field[i]["important_kwd"] = [sres.field[i]["important_kwd"]]
  244. ins_tw = []
  245. for i in sres.ids:
  246. content_ltks = sres.field[i][cfield].split()
  247. title_tks = [t for t in sres.field[i].get("title_tks", "").split() if t]
  248. important_kwd = sres.field[i].get("important_kwd", [])
  249. tks = content_ltks + title_tks + important_kwd
  250. ins_tw.append(tks)
  251. tksim = self.qryr.token_similarity(keywords, ins_tw)
  252. vtsim,_ = rerank_mdl.similarity(query, [rmSpace(" ".join(tks)) for tks in ins_tw])
  253. return tkweight*np.array(tksim) + vtweight*vtsim, tksim, vtsim
  254. def hybrid_similarity(self, ans_embd, ins_embd, ans, inst):
  255. return self.qryr.hybrid_similarity(ans_embd,
  256. ins_embd,
  257. rag_tokenizer.tokenize(ans).split(),
  258. rag_tokenizer.tokenize(inst).split())
  259. def retrieval(self, question, embd_mdl, tenant_ids, kb_ids, page, page_size, similarity_threshold=0.2,
  260. vector_similarity_weight=0.3, top=1024, doc_ids=None, aggs=True, rerank_mdl=None, highlight=False):
  261. ranks = {"total": 0, "chunks": [], "doc_aggs": {}}
  262. if not question:
  263. return ranks
  264. RERANK_PAGE_LIMIT = 3
  265. req = {"kb_ids": kb_ids, "doc_ids": doc_ids, "size": max(page_size*RERANK_PAGE_LIMIT, 128),
  266. "question": question, "vector": True, "topk": top,
  267. "similarity": similarity_threshold,
  268. "available_int": 1}
  269. if page > RERANK_PAGE_LIMIT:
  270. req["page"] = page
  271. req["size"] = page_size
  272. if isinstance(tenant_ids, str):
  273. tenant_ids = tenant_ids.split(",")
  274. sres = self.search(req, [index_name(tid) for tid in tenant_ids], kb_ids, embd_mdl, highlight)
  275. ranks["total"] = sres.total
  276. if page <= RERANK_PAGE_LIMIT:
  277. if rerank_mdl:
  278. sim, tsim, vsim = self.rerank_by_model(rerank_mdl,
  279. sres, question, 1 - vector_similarity_weight, vector_similarity_weight)
  280. else:
  281. sim, tsim, vsim = self.rerank(
  282. sres, question, 1 - vector_similarity_weight, vector_similarity_weight)
  283. idx = np.argsort(sim * -1)[(page-1)*page_size:page*page_size]
  284. else:
  285. sim = tsim = vsim = [1]*len(sres.ids)
  286. idx = list(range(len(sres.ids)))
  287. def floor_sim(score):
  288. return (int(score * 100.)%100)/100.
  289. dim = len(sres.query_vector)
  290. vector_column = f"q_{dim}_vec"
  291. zero_vector = [0.0] * dim
  292. for i in idx:
  293. if floor_sim(sim[i]) < similarity_threshold:
  294. break
  295. if len(ranks["chunks"]) >= page_size:
  296. if aggs:
  297. continue
  298. break
  299. id = sres.ids[i]
  300. chunk = sres.field[id]
  301. dnm = chunk["docnm_kwd"]
  302. did = chunk["doc_id"]
  303. position_int = chunk.get("position_int", [])
  304. d = {
  305. "chunk_id": id,
  306. "content_ltks": chunk["content_ltks"],
  307. "content_with_weight": chunk["content_with_weight"],
  308. "doc_id": chunk["doc_id"],
  309. "docnm_kwd": dnm,
  310. "kb_id": chunk["kb_id"],
  311. "important_kwd": chunk.get("important_kwd", []),
  312. "image_id": chunk.get("img_id", ""),
  313. "similarity": sim[i],
  314. "vector_similarity": vsim[i],
  315. "term_similarity": tsim[i],
  316. "vector": chunk.get(vector_column, zero_vector),
  317. "positions": position_int,
  318. }
  319. if highlight and sres.highlight:
  320. if id in sres.highlight:
  321. d["highlight"] = rmSpace(sres.highlight[id])
  322. else:
  323. d["highlight"] = d["content_with_weight"]
  324. ranks["chunks"].append(d)
  325. if dnm not in ranks["doc_aggs"]:
  326. ranks["doc_aggs"][dnm] = {"doc_id": did, "count": 0}
  327. ranks["doc_aggs"][dnm]["count"] += 1
  328. ranks["doc_aggs"] = [{"doc_name": k,
  329. "doc_id": v["doc_id"],
  330. "count": v["count"]} for k,
  331. v in sorted(ranks["doc_aggs"].items(),
  332. key=lambda x:x[1]["count"] * -1)]
  333. return ranks
  334. def sql_retrieval(self, sql, fetch_size=128, format="json"):
  335. tbl = self.dataStore.sql(sql, fetch_size, format)
  336. return tbl
  337. def chunk_list(self, doc_id: str, tenant_id: str, kb_ids: list[str], max_count=1024, fields=["docnm_kwd", "content_with_weight", "img_id"]):
  338. condition = {"doc_id": doc_id}
  339. res = self.dataStore.search(fields, [], condition, [], OrderByExpr(), 0, max_count, index_name(tenant_id), kb_ids)
  340. dict_chunks = self.dataStore.getFields(res, fields)
  341. return dict_chunks.values()