Spaces:
Running
Running
Upload 3 files
Browse files- app/data_loader.py +50 -0
- app/indexer.py +4 -0
- app/main.py +37 -3
app/data_loader.py
CHANGED
|
@@ -47,6 +47,49 @@ def text_from_article(article: dict[str, Any]) -> str:
|
|
| 47 |
return "\n".join(values)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
def iter_documents() -> Iterator[dict[str, Any]]:
|
| 51 |
for archive_id in range(ARCHIVE_START, ARCHIVE_END + 1):
|
| 52 |
archive_root = PARSED_ROOT / f"archives{archive_id}"
|
|
@@ -65,9 +108,11 @@ def iter_documents() -> Iterator[dict[str, Any]]:
|
|
| 65 |
continue
|
| 66 |
tags_path = article_path.with_suffix(".tags")
|
| 67 |
tags = read_json(tags_path) if tags_path.exists() else []
|
|
|
|
| 68 |
authors = article.get("authors") or []
|
| 69 |
if not isinstance(authors, list):
|
| 70 |
authors = []
|
|
|
|
| 71 |
doc_id = f"{article_id}--{publication_id}"
|
| 72 |
yield {
|
| 73 |
"doc_id": doc_id,
|
|
@@ -81,7 +126,12 @@ def iter_documents() -> Iterator[dict[str, Any]]:
|
|
| 81 |
"title": str(article.get("title") or article_id),
|
| 82 |
"authors": [str(item) for item in authors],
|
| 83 |
"dates": article.get("dates") or [],
|
|
|
|
|
|
|
|
|
|
| 84 |
"tags": tags if isinstance(tags, list) else [],
|
|
|
|
|
|
|
| 85 |
"content": text_from_article(article),
|
| 86 |
"path": str(article_path.relative_to(archive_root)),
|
| 87 |
}
|
|
|
|
| 47 |
return "\n".join(values)
|
| 48 |
|
| 49 |
|
| 50 |
+
def normalize_date_value(item: dict[str, Any]) -> int | None:
|
| 51 |
+
year = item.get("year")
|
| 52 |
+
if not isinstance(year, int):
|
| 53 |
+
return None
|
| 54 |
+
month = item.get("month") if isinstance(item.get("month"), int) else 1
|
| 55 |
+
day = item.get("day") if isinstance(item.get("day"), int) else 1
|
| 56 |
+
return year * 10000 + month * 100 + day
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def normalize_dates(article: dict[str, Any]) -> tuple[int | None, int | None, list[str]]:
|
| 60 |
+
raw_dates = article.get("dates") if isinstance(article.get("dates"), list) else []
|
| 61 |
+
values: list[int] = []
|
| 62 |
+
displays: list[str] = []
|
| 63 |
+
for item in raw_dates:
|
| 64 |
+
if not isinstance(item, dict):
|
| 65 |
+
continue
|
| 66 |
+
value = normalize_date_value(item)
|
| 67 |
+
if value is not None:
|
| 68 |
+
values.append(value)
|
| 69 |
+
year = item.get("year") if item.get("year") is not None else "----"
|
| 70 |
+
month = item.get("month") if item.get("month") is not None else "--"
|
| 71 |
+
day = item.get("day") if item.get("day") is not None else "--"
|
| 72 |
+
displays.append(f"{year}/{month}/{day}")
|
| 73 |
+
return (min(values) if values else None, max(values) if values else None, displays)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def normalize_tags(tags: Any) -> tuple[list[str], list[str]]:
|
| 77 |
+
if not isinstance(tags, list):
|
| 78 |
+
return [], []
|
| 79 |
+
names: list[str] = []
|
| 80 |
+
types: list[str] = []
|
| 81 |
+
for item in tags:
|
| 82 |
+
if not isinstance(item, dict):
|
| 83 |
+
continue
|
| 84 |
+
name = item.get("name")
|
| 85 |
+
tag_type = item.get("type")
|
| 86 |
+
if name is not None:
|
| 87 |
+
names.append(str(name))
|
| 88 |
+
if tag_type is not None:
|
| 89 |
+
types.append(str(tag_type))
|
| 90 |
+
return names, types
|
| 91 |
+
|
| 92 |
+
|
| 93 |
def iter_documents() -> Iterator[dict[str, Any]]:
|
| 94 |
for archive_id in range(ARCHIVE_START, ARCHIVE_END + 1):
|
| 95 |
archive_root = PARSED_ROOT / f"archives{archive_id}"
|
|
|
|
| 108 |
continue
|
| 109 |
tags_path = article_path.with_suffix(".tags")
|
| 110 |
tags = read_json(tags_path) if tags_path.exists() else []
|
| 111 |
+
tag_names, tag_types = normalize_tags(tags)
|
| 112 |
authors = article.get("authors") or []
|
| 113 |
if not isinstance(authors, list):
|
| 114 |
authors = []
|
| 115 |
+
date_min, date_max, date_display = normalize_dates(article)
|
| 116 |
doc_id = f"{article_id}--{publication_id}"
|
| 117 |
yield {
|
| 118 |
"doc_id": doc_id,
|
|
|
|
| 126 |
"title": str(article.get("title") or article_id),
|
| 127 |
"authors": [str(item) for item in authors],
|
| 128 |
"dates": article.get("dates") or [],
|
| 129 |
+
"date_min": date_min,
|
| 130 |
+
"date_max": date_max,
|
| 131 |
+
"date_display": date_display,
|
| 132 |
"tags": tags if isinstance(tags, list) else [],
|
| 133 |
+
"tag_names": tag_names,
|
| 134 |
+
"tag_types": tag_types,
|
| 135 |
"content": text_from_article(article),
|
| 136 |
"path": str(article_path.relative_to(archive_root)),
|
| 137 |
}
|
app/indexer.py
CHANGED
|
@@ -34,6 +34,10 @@ def index_mapping() -> dict:
|
|
| 34 |
"archive_id": {"type": "integer"},
|
| 35 |
"title": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
| 36 |
"authors": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
"content": {"type": "text", "analyzer": "text_mixed"},
|
| 38 |
"path": {"type": "keyword"},
|
| 39 |
}
|
|
|
|
| 34 |
"archive_id": {"type": "integer"},
|
| 35 |
"title": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
| 36 |
"authors": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
| 37 |
+
"tag_names": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
| 38 |
+
"tag_types": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
|
| 39 |
+
"date_min": {"type": "integer"},
|
| 40 |
+
"date_max": {"type": "integer"},
|
| 41 |
"content": {"type": "text", "analyzer": "text_mixed"},
|
| 42 |
"path": {"type": "keyword"},
|
| 43 |
}
|
app/main.py
CHANGED
|
@@ -25,6 +25,12 @@ class SearchRequest(BaseModel):
|
|
| 25 |
page_size: int = 20
|
| 26 |
exact: bool = True
|
| 27 |
source: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def has_wildcard(query: str) -> bool:
|
|
@@ -36,6 +42,18 @@ def build_query(body: SearchRequest) -> dict[str, Any]:
|
|
| 36 |
filters: list[dict[str, Any]] = []
|
| 37 |
if body.source:
|
| 38 |
filters.append({"term": {"publication_name.keyword": body.source}})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
if not q:
|
| 41 |
query: dict[str, Any] = {"match_all": {}}
|
|
@@ -78,6 +96,8 @@ def trim_hit(hit: dict[str, Any]) -> dict[str, Any]:
|
|
| 78 |
"publication_name": source.get("publication_name"),
|
| 79 |
"archive_id": source.get("archive_id"),
|
| 80 |
"publication_type": source.get("publication_type"),
|
|
|
|
|
|
|
| 81 |
"title": source.get("title"),
|
| 82 |
"authors": source.get("authors") or [],
|
| 83 |
"path": source.get("path"),
|
|
@@ -96,10 +116,22 @@ def sources():
|
|
| 96 |
data = es.search(
|
| 97 |
index=INDEX_NAME,
|
| 98 |
size=0,
|
| 99 |
-
aggs={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
)
|
| 101 |
-
|
| 102 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
@app.post("/api/search")
|
|
@@ -129,6 +161,8 @@ def preview(doc_id: str):
|
|
| 129 |
"title": source.get("title"),
|
| 130 |
"publication_name": source.get("publication_name"),
|
| 131 |
"publication_type": source.get("publication_type"),
|
|
|
|
|
|
|
| 132 |
"authors": source.get("authors") or [],
|
| 133 |
"content": source.get("content") or "",
|
| 134 |
"article_id": source.get("article_id"),
|
|
|
|
| 25 |
page_size: int = 20
|
| 26 |
exact: bool = True
|
| 27 |
source: str | None = None
|
| 28 |
+
author: str | None = None
|
| 29 |
+
tag: str | None = None
|
| 30 |
+
archive_id: int | None = None
|
| 31 |
+
publication_type: str | None = None
|
| 32 |
+
year_from: int | None = None
|
| 33 |
+
year_to: int | None = None
|
| 34 |
|
| 35 |
|
| 36 |
def has_wildcard(query: str) -> bool:
|
|
|
|
| 42 |
filters: list[dict[str, Any]] = []
|
| 43 |
if body.source:
|
| 44 |
filters.append({"term": {"publication_name.keyword": body.source}})
|
| 45 |
+
if body.author:
|
| 46 |
+
filters.append({"term": {"authors.keyword": body.author}})
|
| 47 |
+
if body.tag:
|
| 48 |
+
filters.append({"term": {"tag_names.keyword": body.tag}})
|
| 49 |
+
if body.archive_id is not None:
|
| 50 |
+
filters.append({"term": {"archive_id": body.archive_id}})
|
| 51 |
+
if body.publication_type:
|
| 52 |
+
filters.append({"term": {"publication_type": body.publication_type}})
|
| 53 |
+
if body.year_from is not None:
|
| 54 |
+
filters.append({"range": {"date_max": {"gte": body.year_from * 10000}}})
|
| 55 |
+
if body.year_to is not None:
|
| 56 |
+
filters.append({"range": {"date_min": {"lte": body.year_to * 10000 + 1231}}})
|
| 57 |
|
| 58 |
if not q:
|
| 59 |
query: dict[str, Any] = {"match_all": {}}
|
|
|
|
| 96 |
"publication_name": source.get("publication_name"),
|
| 97 |
"archive_id": source.get("archive_id"),
|
| 98 |
"publication_type": source.get("publication_type"),
|
| 99 |
+
"date_display": source.get("date_display") or [],
|
| 100 |
+
"tag_names": source.get("tag_names") or [],
|
| 101 |
"title": source.get("title"),
|
| 102 |
"authors": source.get("authors") or [],
|
| 103 |
"path": source.get("path"),
|
|
|
|
| 116 |
data = es.search(
|
| 117 |
index=INDEX_NAME,
|
| 118 |
size=0,
|
| 119 |
+
aggs={
|
| 120 |
+
"sources": {"terms": {"field": "publication_name.keyword", "size": 500}},
|
| 121 |
+
"authors": {"terms": {"field": "authors.keyword", "size": 300}},
|
| 122 |
+
"tags": {"terms": {"field": "tag_names.keyword", "size": 300}},
|
| 123 |
+
"archives": {"terms": {"field": "archive_id", "size": 64}},
|
| 124 |
+
"types": {"terms": {"field": "publication_type", "size": 32}},
|
| 125 |
+
},
|
| 126 |
)
|
| 127 |
+
aggs = data.get("aggregations", {})
|
| 128 |
+
return {
|
| 129 |
+
"sources": [{"name": item["key"], "count": item["doc_count"]} for item in aggs.get("sources", {}).get("buckets", [])],
|
| 130 |
+
"authors": [{"name": item["key"], "count": item["doc_count"]} for item in aggs.get("authors", {}).get("buckets", [])],
|
| 131 |
+
"tags": [{"name": item["key"], "count": item["doc_count"]} for item in aggs.get("tags", {}).get("buckets", [])],
|
| 132 |
+
"archives": [{"name": str(item["key"]), "value": item["key"], "count": item["doc_count"]} for item in aggs.get("archives", {}).get("buckets", [])],
|
| 133 |
+
"types": [{"name": item["key"], "count": item["doc_count"]} for item in aggs.get("types", {}).get("buckets", [])],
|
| 134 |
+
}
|
| 135 |
|
| 136 |
|
| 137 |
@app.post("/api/search")
|
|
|
|
| 161 |
"title": source.get("title"),
|
| 162 |
"publication_name": source.get("publication_name"),
|
| 163 |
"publication_type": source.get("publication_type"),
|
| 164 |
+
"date_display": source.get("date_display") or [],
|
| 165 |
+
"tag_names": source.get("tag_names") or [],
|
| 166 |
"authors": source.get("authors") or [],
|
| 167 |
"content": source.get("content") or "",
|
| 168 |
"article_id": source.get("article_id"),
|