ahmadsayadi's picture
feat: add keywords extraction, NER, project digest endpoints
f39a9fa
Raw
History Blame Contribute Delete
3.5 kB
"""
Named Entity Recognition (NER) — rule-based + pattern enhanced.
Lebih presisi dari regex sederhana: gunakan gazetteer + context patterns.
"""
from typing import List, Dict
import re
# Gazetteer Indonesia (expandable)
PERSON_TITLES = [
"presiden", "menteri", "gubernur", "bupati", "walikota", "calon",
"ketua", "wakil", "direktur", "komisaris", "jenderal", "kolonel",
"mayor", "kapten", "prof", "dr", "ir", "haji", "ustaz", "kyai",
]
ORG_KEYWORDS = [
"kementerian", "badan", "dewan", "komisi", "partai", "pt", "tbk",
"universitas", "institut", "polri", "tni", "bpk", "kpk", "ojk",
"bi", "bps", "bmkg", "bnpb", "baznas", "mui", "nu", "muhammadiyah",
"perserikatan", "organisasi", "perusahaan", "bank", "asosiasi",
]
LOCATION_KEYWORDS = [
"jakarta", "surabaya", "bandung", "medan", "semarang", "makassar",
"yogyakarta", "denpasar", "palembang", "manado", "padang", "solo",
"indonesia", "jawa", "sumatera", "kalimantan", "sulawesi", "papua",
"bali", "ntt", "ntb", "aceh", "riau", "lampung", "maluku",
"provinsi", "kabupaten", "kota", "desa", "kecamatan",
]
def _is_capitalized_phrase(text: str, start: int, end: int) -> bool:
"""Cek apakah span memiliki kata yang diawali huruf besar."""
span = text[start:end]
words = span.split()
return any(w[0].isupper() for w in words if w)
def extract_entities(text: str) -> Dict[str, List[str]]:
"""Extract persons, organizations, locations dari text."""
persons = set()
organizations = set()
locations = set()
text_lower = text.lower()
words = text.split()
# Pattern: Title + Capitalized Name (person)
for i, word in enumerate(words):
word_lower = word.lower().strip(".,;:!?\"'()")
if word_lower in PERSON_TITLES and i + 1 < len(words):
# Ambil 1-3 kata setelah title sebagai nama
name_parts = []
for j in range(i + 1, min(i + 4, len(words))):
w = words[j].strip(".,;:!?\"'()")
if w and w[0].isupper():
name_parts.append(w)
else:
break
if name_parts:
persons.add(" ".join(name_parts))
# Pattern: Capitalized consecutive words (potential names/orgs)
cap_pattern = re.finditer(r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b', text)
for match in cap_pattern:
phrase = match.group(1)
phrase_lower = phrase.lower()
# Classify based on context
if any(kw in phrase_lower for kw in ORG_KEYWORDS):
organizations.add(phrase)
elif any(kw in phrase_lower for kw in LOCATION_KEYWORDS):
locations.add(phrase)
elif len(phrase.split()) <= 3:
persons.add(phrase)
# Direct keyword matching for organizations
for kw in ORG_KEYWORDS:
pattern = re.finditer(rf'\b{re.escape(kw)}\s+([A-Z][a-zA-Z\s]{{2,30}})', text, re.IGNORECASE)
for m in pattern:
organizations.add(m.group(0).strip())
# Location extraction
for kw in LOCATION_KEYWORDS:
if kw in text_lower:
locations.add(kw.title())
return {
"persons": list(persons)[:20],
"organizations": list(organizations)[:15],
"locations": list(locations)[:15],
}
def extract_batch(items: List) -> List[Dict]:
results = []
for item in items:
entities = extract_entities(item.text)
results.append({"id": item.id, "entities": entities})
return results