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

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -43
app.py CHANGED
@@ -46,7 +46,11 @@ source_records_map: dict[str, list[int]] = {}
46
  extension_counts: dict[str, int] = {}
47
  source_extension_counts: dict[str, dict[str, int]] = {}
48
  word_index: dict[str, set[int]] = {}
 
 
 
49
  TOKEN_RE = re.compile(r"[a-z0-9]+|[\u4e00-\u9fff\u3400-\u4dbf]+")
 
50
  TOKENIZER_VERSION = "cjk-bigram-boundary-fts5-v6-snippet-anchors"
51
  LITERAL_PREFIX = "\0literal:"
52
  API_CACHE_TTL_SECONDS = 120
@@ -81,6 +85,7 @@ def index_tokens(text: str) -> set[str]:
81
  tokens.add(part)
82
  return tokens
83
 
 
84
  def query_tokens(text: str) -> list[str]:
85
  tokens = []
86
  for part in query_terms(text):
@@ -90,6 +95,7 @@ def query_tokens(text: str) -> list[str]:
90
  tokens.append(part)
91
  return list(dict.fromkeys(tokens))
92
 
 
93
  def literal_query_tokens(text: str) -> list[str]:
94
  normalized = normalize_text(text)
95
  return list(dict.fromkeys(
@@ -98,6 +104,7 @@ def literal_query_tokens(text: str) -> list[str]:
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)):
@@ -724,8 +731,11 @@ def load_json_gz(path: Path):
724
  return json.loads(gzip.decompress(path.read_bytes()).decode("utf-8"))
725
 
726
  def build_indexes() -> None:
727
- global word_index, source_records_map, extension_counts, source_extension_counts, name_order, size_order, source_name_order, source_size_order
728
  word_index = {}
 
 
 
729
  source_records_map = {}
730
  extension_counts = {}
731
  source_extension_counts = {}
@@ -745,6 +755,12 @@ def build_indexes() -> None:
745
  rec["_rank_path_key"] = rec["display_rel_path"].lower()
746
  for token in tokens:
747
  word_index.setdefault(token, set()).add(idx)
 
 
 
 
 
 
748
  name_order = sorted(
749
  range(len(records)),
750
  key=lambda idx: (records[idx]["display_rel_path"].lower(), records[idx]["doc_id"]),
@@ -900,15 +916,45 @@ def add_summaries(items: list[dict], query: str = "", matched_snippets: bool = F
900
  item["snippet_complete"] = bool(payload) or (not needs_matched_snippet and item["doc_id"] in summaries)
901
  return items
902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
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):
@@ -917,16 +963,10 @@ def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
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
924
- for token in query_tokens(q):
925
- token_indices = word_index.get(token, set())
926
- candidate_set = set(token_indices) if candidate_set is None else candidate_set & token_indices
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)
@@ -934,19 +974,11 @@ def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
934
  terms = query_terms(q)
935
  if not terms:
936
  return set()
937
- if any(re.fullmatch(r"[a-z0-9]+", term) for term in terms):
938
- return {
939
- idx for idx, rec in enumerate(records)
940
- if all(term in rec[field] for term in terms)
941
- }
942
- candidates = None
943
- for token in query_tokens(q):
944
- token_indices = word_index.get(token, set())
945
- candidates = set(token_indices) if candidates is None else candidates & token_indices
946
- if not candidates:
947
- return set()
948
  return {
949
- idx for idx in (candidates or set())
950
  if all(term in records[idx][field] for term in terms)
951
  }
952
 
@@ -1276,24 +1308,27 @@ def run_search(body: SearchRequest, sources_filter=None, timings=None):
1276
  body.search_paths,
1277
  body.fulltext,
1278
  )
1279
- result = cached_search_payload(
1280
- key,
1281
- lambda: fulltext_search(
1282
- body.q, selected_sources, body.folders, body.min_size, body.max_size,
1283
- body.page, body.page_size, body.sort, body.exact, body.search_paths,
1284
- timings,
1285
- ) if body.fulltext else search(
1286
- body.q, selected_sources, body.folders, body.min_size, body.max_size,
1287
- body.page, body.page_size, body.sort, body.exact, body.search_paths,
1288
- timings,
1289
- ),
1290
- timings,
1291
- )
1292
- payload = dict(result)
1293
- requested_sources = set(selected_sources or source_counts)
1294
- payload["indexing"] = bool(body.fulltext and not requested_sources.issubset(ready_fulltext_sources()))
1295
- payload["index_generation"] = fulltext_generation
1296
- return payload
 
 
 
1297
 
1298
  @app.post("/api/search")
1299
 
 
46
  extension_counts: dict[str, int] = {}
47
  source_extension_counts: dict[str, dict[str, int]] = {}
48
  word_index: dict[str, set[int]] = {}
49
+ latin_all_index: dict[str, set[int]] = {}
50
+ latin_file_index: dict[str, set[int]] = {}
51
+ latin_vocabulary_all: set[str] = set()
52
  TOKEN_RE = re.compile(r"[a-z0-9]+|[\u4e00-\u9fff\u3400-\u4dbf]+")
53
+ LATIN_RE = re.compile(r"[a-z0-9]+")
54
  TOKENIZER_VERSION = "cjk-bigram-boundary-fts5-v6-snippet-anchors"
55
  LITERAL_PREFIX = "\0literal:"
56
  API_CACHE_TTL_SECONDS = 120
 
85
  tokens.add(part)
86
  return tokens
87
 
88
+ @lru_cache(maxsize=4096)
89
  def query_tokens(text: str) -> list[str]:
90
  tokens = []
91
  for part in query_terms(text):
 
95
  tokens.append(part)
96
  return list(dict.fromkeys(tokens))
97
 
98
+ @lru_cache(maxsize=4096)
99
  def literal_query_tokens(text: str) -> list[str]:
100
  normalized = normalize_text(text)
101
  return list(dict.fromkeys(
 
104
  if not TOKEN_RE.fullmatch(normalized[index + 1])
105
  ))
106
 
107
+ @lru_cache(maxsize=4096)
108
  def wildcard_required_tokens(text: str) -> list[str]:
109
  tokens = []
110
  for fixed_part in re.split(r"[*?]+", normalize_text(text)):
 
731
  return json.loads(gzip.decompress(path.read_bytes()).decode("utf-8"))
732
 
733
  def build_indexes() -> None:
734
+ global word_index, latin_all_index, latin_file_index, latin_vocabulary_all, source_records_map, extension_counts, source_extension_counts, name_order, size_order, source_name_order, source_size_order
735
  word_index = {}
736
+ latin_all_index = {}
737
+ latin_file_index = {}
738
+ latin_vocabulary_all = set()
739
  source_records_map = {}
740
  extension_counts = {}
741
  source_extension_counts = {}
 
755
  rec["_rank_path_key"] = rec["display_rel_path"].lower()
756
  for token in tokens:
757
  word_index.setdefault(token, set()).add(idx)
758
+ if LATIN_RE.fullmatch(token):
759
+ latin_all_index.setdefault(token, set()).add(idx)
760
+ latin_vocabulary_all.add(token)
761
+ for token in TOKEN_RE.findall(rec["_file_search_text"]):
762
+ if LATIN_RE.fullmatch(token):
763
+ latin_file_index.setdefault(token, set()).add(idx)
764
  name_order = sorted(
765
  range(len(records)),
766
  key=lambda idx: (records[idx]["display_rel_path"].lower(), records[idx]["doc_id"]),
 
916
  item["snippet_complete"] = bool(payload) or (not needs_matched_snippet and item["doc_id"] in summaries)
917
  return items
918
 
919
+ def latin_substring_candidates(term: str, search_paths: bool) -> set[int]:
920
+ index = latin_all_index if search_paths else latin_file_index
921
+ candidates: set[int] = set()
922
+ for token in latin_vocabulary_all:
923
+ if term in token:
924
+ candidates |= index.get(token, ())
925
+ return candidates
926
+
927
+ def metadata_prefilter(terms: list[str], search_paths: bool) -> set[int] | None:
928
+ candidates: set[int] | None = None
929
+ for term in terms:
930
+ if LATIN_RE.fullmatch(term):
931
+ term_candidates = latin_substring_candidates(term, search_paths)
932
+ else:
933
+ term_candidates = None
934
+ for token in query_tokens(term):
935
+ indices = word_index.get(token)
936
+ if not indices:
937
+ term_candidates = set()
938
+ break
939
+ term_candidates = set(indices) if term_candidates is None else term_candidates & indices
940
+ if not term_candidates:
941
+ break
942
+ if term_candidates is None:
943
+ continue
944
+ candidates = term_candidates if candidates is None else candidates & term_candidates
945
+ if not candidates:
946
+ return set()
947
+ return candidates
948
+
949
  def metadata_matches(q: str, exact: bool, search_paths: bool) -> set[int]:
950
  field = "_search_text" if search_paths else "_file_search_text"
951
  if exact:
952
  terms = query_terms(q)
953
  normalized_query, pattern = compile_exact_query(q)
954
+ # CJK literals necessarily contain all of their indexed unigram/bigram
955
+ # tokens, so candidate intersection is lossless there. Latin substrings
956
+ # are resolved through the per-field Latin token index, and
957
+ # punctuation-only queries retain the full-range scan.
958
  if has_wildcard_query(q):
959
  candidate_set = None
960
  for token in wildcard_required_tokens(normalized_query):
 
963
  if not candidate_set:
964
  return set()
965
  candidates = candidate_set if candidate_set is not None else range(len(records))
966
+ elif not terms:
967
  candidates = range(len(records))
968
  else:
969
+ candidates = metadata_prefilter(terms, search_paths) or set()
 
 
 
 
 
 
970
  return {
971
  idx for idx in candidates
972
  if matches_normalized_exact_query(records[idx][field], normalized_query, pattern)
 
974
  terms = query_terms(q)
975
  if not terms:
976
  return set()
977
+ candidates = metadata_prefilter(terms, search_paths)
978
+ if candidates is None:
979
+ candidates = range(len(records))
 
 
 
 
 
 
 
 
980
  return {
981
+ idx for idx in candidates
982
  if all(term in records[idx][field] for term in terms)
983
  }
984
 
 
1308
  body.search_paths,
1309
  body.fulltext,
1310
  )
1311
+ def build_payload():
1312
+ result = (
1313
+ fulltext_search(
1314
+ body.q, selected_sources, body.folders, body.min_size, body.max_size,
1315
+ body.page, body.page_size, body.sort, body.exact, body.search_paths,
1316
+ timings,
1317
+ )
1318
+ if body.fulltext
1319
+ else search(
1320
+ body.q, selected_sources, body.folders, body.min_size, body.max_size,
1321
+ body.page, body.page_size, body.sort, body.exact, body.search_paths,
1322
+ timings,
1323
+ )
1324
+ )
1325
+ payload = dict(result)
1326
+ requested_sources = set(selected_sources or source_counts)
1327
+ payload["indexing"] = bool(body.fulltext and not requested_sources.issubset(ready_fulltext_sources()))
1328
+ payload["index_generation"] = fulltext_generation
1329
+ return payload
1330
+
1331
+ return cached_search_payload(key, build_payload, timings)
1332
 
1333
  @app.post("/api/search")
1334