vomebook commited on
Commit
66d85bc
·
verified ·
1 Parent(s): 534bba5

Optimize CJK wildcard search candidates

Browse files

Reuse normalized metadata text and prefilter wildcard candidates through existing CJK postings without changing tokenizer or index format.

Files changed (1) hide show
  1. app.py +39 -10
app.py CHANGED
@@ -98,6 +98,14 @@ def literal_query_tokens(text: str) -> list[str]:
98
  if not TOKEN_RE.fullmatch(normalized[index + 1])
99
  ))
100
 
 
 
 
 
 
 
 
 
101
  def decode_doc_numbers(payload: bytes, candidates: set[int] | None = None) -> set[int]:
102
  results = set()
103
  number = shift = previous = 0
@@ -286,15 +294,18 @@ class FulltextDatabases:
286
  normalized_query = normalize_text(query)
287
  try:
288
  if has_wildcard_query(normalized_query):
 
 
289
  fixed_parts = [part for part in re.split(r"[*?]+", normalized_query) if len(part) >= 3]
290
  if fixed_parts:
291
  match_query = " AND ".join(f'"{part.replace(chr(34), chr(34) * 2)}"' for part in fixed_parts)
292
- candidates = {
293
  int(row[0]) for row in connection.execute(
294
  "SELECT rowid FROM content_fts WHERE content_fts MATCH ?", (match_query,)
295
  )
296
  }
297
- else:
 
298
  candidates = {
299
  int(row[0]) for row in connection.execute("SELECT rowid FROM content_fts")
300
  }
@@ -581,13 +592,19 @@ fulltext_databases: Optional[FulltextDatabases] = None
581
  def tokenize(text: str) -> list[str]:
582
  return query_terms(text)
583
 
 
 
 
 
 
 
 
 
 
 
584
  def matches_exact_query(text: str, query: str) -> bool:
585
- text = normalize_text(text)
586
- query = normalize_text(query)
587
- if "*" in query or "?" in query:
588
- pattern = re.escape(query).replace(r"\*", ".*?").replace(r"\?", ".")
589
- return re.search(pattern, text, re.DOTALL) is not None
590
- return query in text
591
 
592
  def has_wildcard_query(query: str) -> bool:
593
  return "*" in query or "?" in query
@@ -887,11 +904,20 @@ def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
887
  field = "_search_text" if search_paths else "_file_search_text"
888
  if exact:
889
  terms = query_terms(q)
 
890
  # Whole-token postings are not a lossless prefilter for Latin
891
  # substrings or punctuation-only queries. CJK literals necessarily
892
  # contain all of their indexed unigram/bigram tokens, so retain the
893
  # fast candidate intersection for that common path.
894
- if has_wildcard_query(q) or not terms or any(re.fullmatch(r"[a-z0-9]+", term) for term in terms):
 
 
 
 
 
 
 
 
895
  candidates = range(len(records))
896
  else:
897
  candidate_set = None
@@ -901,7 +927,10 @@ def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
901
  if not candidate_set:
902
  return set()
903
  candidates = candidate_set or set()
904
- return {idx for idx in candidates if matches_exact_query(records[idx][field], q)}
 
 
 
905
  terms = query_terms(q)
906
  if not terms:
907
  return set()
 
98
  if not TOKEN_RE.fullmatch(normalized[index + 1])
99
  ))
100
 
101
+ def wildcard_required_tokens(text: str) -> list[str]:
102
+ tokens = []
103
+ for fixed_part in re.split(r"[*?]+", normalize_text(text)):
104
+ for term in TOKEN_RE.findall(fixed_part):
105
+ if re.fullmatch(r"[\u4e00-\u9fff\u3400-\u4dbf]+", term):
106
+ tokens.extend(query_tokens(term))
107
+ return list(dict.fromkeys(tokens))
108
+
109
  def decode_doc_numbers(payload: bytes, candidates: set[int] | None = None) -> set[int]:
110
  results = set()
111
  number = shift = previous = 0
 
294
  normalized_query = normalize_text(query)
295
  try:
296
  if has_wildcard_query(normalized_query):
297
+ required_tokens = wildcard_required_tokens(normalized_query) if version == TOKENIZER_VERSION else []
298
+ candidates = self._search_postings(connection, has_doc_counts, required_tokens) if required_tokens else None
299
  fixed_parts = [part for part in re.split(r"[*?]+", normalized_query) if len(part) >= 3]
300
  if fixed_parts:
301
  match_query = " AND ".join(f'"{part.replace(chr(34), chr(34) * 2)}"' for part in fixed_parts)
302
+ fts_candidates = {
303
  int(row[0]) for row in connection.execute(
304
  "SELECT rowid FROM content_fts WHERE content_fts MATCH ?", (match_query,)
305
  )
306
  }
307
+ candidates = fts_candidates if candidates is None else candidates & fts_candidates
308
+ elif candidates is None:
309
  candidates = {
310
  int(row[0]) for row in connection.execute("SELECT rowid FROM content_fts")
311
  }
 
592
  def tokenize(text: str) -> list[str]:
593
  return query_terms(text)
594
 
595
+ def compile_exact_query(query: str) -> tuple[str, re.Pattern | None]:
596
+ normalized_query = normalize_text(query)
597
+ if has_wildcard_query(normalized_query):
598
+ expression = re.escape(normalized_query).replace(r"\*", ".*?").replace(r"\?", ".")
599
+ return normalized_query, re.compile(expression, re.DOTALL)
600
+ return normalized_query, None
601
+
602
+ def matches_normalized_exact_query(text: str, normalized_query: str, pattern: re.Pattern | None = None) -> bool:
603
+ return pattern.search(text) is not None if pattern is not None else normalized_query in text
604
+
605
  def matches_exact_query(text: str, query: str) -> bool:
606
+ normalized_query, pattern = compile_exact_query(query)
607
+ return matches_normalized_exact_query(normalize_text(text), normalized_query, pattern)
 
 
 
 
608
 
609
  def has_wildcard_query(query: str) -> bool:
610
  return "*" in query or "?" in query
 
904
  field = "_search_text" if search_paths else "_file_search_text"
905
  if exact:
906
  terms = query_terms(q)
907
+ normalized_query, pattern = compile_exact_query(q)
908
  # Whole-token postings are not a lossless prefilter for Latin
909
  # substrings or punctuation-only queries. CJK literals necessarily
910
  # contain all of their indexed unigram/bigram tokens, so retain the
911
  # fast candidate intersection for that common path.
912
+ if has_wildcard_query(q):
913
+ candidate_set = None
914
+ for token in wildcard_required_tokens(normalized_query):
915
+ token_indices = word_index.get(token, set())
916
+ candidate_set = set(token_indices) if candidate_set is None else candidate_set & token_indices
917
+ if not candidate_set:
918
+ return set()
919
+ candidates = candidate_set if candidate_set is not None else range(len(records))
920
+ elif not terms or any(re.fullmatch(r"[a-z0-9]+", term) for term in terms):
921
  candidates = range(len(records))
922
  else:
923
  candidate_set = None
 
927
  if not candidate_set:
928
  return set()
929
  candidates = candidate_set or set()
930
+ return {
931
+ idx for idx in candidates
932
+ if matches_normalized_exact_query(records[idx][field], normalized_query, pattern)
933
+ }
934
  terms = query_terms(q)
935
  if not terms:
936
  return set()