SPB Git
7.8 KB · 230 lines python
1# ============================================================2# Project: EDGAR Research Database3# Description: SEC EDGAR Research Infrastructure4#5# Authors:6# - Manel Kammoun — manel.kammoun@uqo.ca7# - Djerrycharli Tandjambianda — djerrycharli.tandjambianda@uqo.ca8# - Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca9#10# Institution:11# Université du Québec en Outaouais (UQO)12#13# License:14# Research / Academic Use15# ============================================================1617from __future__ import annotations1819from typing import TYPE_CHECKING2021import numpy as np22import pandas as pd2324from src.utils.logging import get_logger2526if TYPE_CHECKING:27    from src.embeddings.manager import EmbeddingManager28    from src.storage.database import DatabaseManager2930logger = get_logger("search.semantic")313233class SemanticSearch:3435    def __init__(self, db: DatabaseManager, embedding_manager: EmbeddingManager) -> None:36        self.db = db37        self.emb = embedding_manager3839    def search(40        self,41        query: str,42        top_k: int = 20,43        form_type: str | None = None,44        ticker: str | None = None,45        start_date: str | None = None,46        end_date: str | None = None,47        source_type: str = "chunk",48    ) -> pd.DataFrame:49        model_name = self.emb.model_name50        query_vector = self.emb.registry.encode([query], model_name=model_name, show_progress=False)51        query_vector = query_vector[0]5253        conditions: list[str] = ["em.source_type = ?"]54        params: list = [source_type]5556        if form_type:57            conditions.append("f.form_type = ?")58            params.append(form_type)59        if ticker:60            conditions.append("f.ticker = ?")61            params.append(ticker)62        if start_date:63            conditions.append("f.filing_date >= ?")64            params.append(start_date)65        if end_date:66            conditions.append("f.filing_date <= ?")67            params.append(end_date)6869        conditions.append("em.model_name = ?")70        params.append(model_name)7172        where_clause = " AND ".join(conditions)7374        if source_type == "chunk":75            sql = f"""76                SELECT77                    em.source_id,78                    c.chunk_text AS text,79                    c.accession_number,80                    f.ticker,81                    f.company_name,82                    f.filing_date,83                    f.form_type,84                    s.section_name85                FROM embeddings_metadata em86                JOIN filing_chunks c ON em.source_id = c.chunk_id87                JOIN filings f ON c.accession_number = f.accession_number88                LEFT JOIN filing_sections s ON c.section_id = s.section_id89                WHERE {where_clause}90            """91        else:92            sql = f"""93                SELECT94                    em.source_id,95                    sec.text,96                    sec.accession_number,97                    f.ticker,98                    f.company_name,99                    f.filing_date,100                    f.form_type,101                    sec.section_name102                FROM embeddings_metadata em103                JOIN filing_sections sec ON em.source_id = sec.section_id104                JOIN filings f ON sec.accession_number = f.accession_number105                WHERE {where_clause}106            """107108        candidates_df = self.db.fetch_df(sql, params)109110        if candidates_df.empty:111            logger.info("No candidates found for query")112            return pd.DataFrame()113114        source_ids = candidates_df["source_id"].tolist()115        vectors = self.emb.get_vectors(source_ids, model_name=model_name)116117        if vectors.size == 0:118            logger.warning("No embedding vectors found for candidates")119            return pd.DataFrame()120121        similarities = self._cosine_similarity(query_vector, vectors)122        candidates_df = candidates_df.copy()123        candidates_df["similarity_score"] = similarities124125        results = (126            candidates_df127            .sort_values("similarity_score", ascending=False)128            .head(top_k)129            .reset_index(drop=True)130        )131132        logger.info(133            "Search returned %d results (top score=%.4f)",134            len(results),135            results["similarity_score"].iloc[0] if not results.empty else 0.0,136        )137        return results138139    def search_similar(self, source_id: str, top_k: int = 10) -> pd.DataFrame:140        meta_sql = "SELECT source_type, model_name FROM embeddings_metadata WHERE source_id = ? LIMIT 1"141        meta_df = self.db.fetch_df(meta_sql, [source_id])142143        if meta_df.empty:144            logger.warning("Source ID '%s' not found in embeddings metadata", source_id)145            return pd.DataFrame()146147        source_type = meta_df["source_type"].iloc[0]148        model_name = meta_df["model_name"].iloc[0]149150        source_vector = self.emb.get_vectors([source_id], model_name=model_name)151        if source_vector.size == 0:152            return pd.DataFrame()153        source_vector = source_vector[0]154155        all_meta_sql = (156            "SELECT source_id FROM embeddings_metadata "157            "WHERE source_type = ? AND model_name = ? AND source_id != ?"158        )159        all_meta_df = self.db.fetch_df(all_meta_sql, [source_type, model_name, source_id])160161        if all_meta_df.empty:162            return pd.DataFrame()163164        candidate_ids = all_meta_df["source_id"].tolist()165        vectors = self.emb.get_vectors(candidate_ids, model_name=model_name)166167        if vectors.size == 0:168            return pd.DataFrame()169170        similarities = self._cosine_similarity(source_vector, vectors)171172        if source_type == "chunk":173            detail_sql = """174                SELECT c.chunk_id AS source_id, c.chunk_text AS text, c.accession_number,175                       f.ticker, f.company_name, f.filing_date, f.form_type176                FROM filing_chunks c177                JOIN filings f ON c.accession_number = f.accession_number178                WHERE c.chunk_id IN ({})179            """.format(", ".join(["?"] * len(candidate_ids)))180        else:181            detail_sql = """182                SELECT s.section_id AS source_id, s.text, s.accession_number,183                       f.ticker, f.company_name, f.filing_date, f.form_type184                FROM filing_sections s185                JOIN filings f ON s.accession_number = f.accession_number186                WHERE s.section_id IN ({})187            """.format(", ".join(["?"] * len(candidate_ids)))188189        detail_df = self.db.fetch_df(detail_sql, candidate_ids)190191        sim_df = pd.DataFrame({"source_id": candidate_ids, "similarity_score": similarities})192        results = detail_df.merge(sim_df, on="source_id")193        results = (194            results195            .sort_values("similarity_score", ascending=False)196            .head(top_k)197            .reset_index(drop=True)198        )199        return results200201    def search_companies(self, query: str, top_k: int = 5) -> pd.DataFrame:202        results = self.search(query, top_k=200)203204        if results.empty:205            return pd.DataFrame()206207        company_agg = (208            results209            .groupby(["ticker", "company_name"])210            .agg(211                avg_similarity=("similarity_score", "mean"),212                max_similarity=("similarity_score", "max"),213                match_count=("similarity_score", "count"),214            )215            .reset_index()216            .sort_values("avg_similarity", ascending=False)217            .head(top_k)218            .reset_index(drop=True)219        )220        return company_agg221222    @staticmethod223    def _cosine_similarity(query_vec: np.ndarray, matrix: np.ndarray) -> np.ndarray:224        query_norm = np.linalg.norm(query_vec)225        if query_norm == 0:226            return np.zeros(matrix.shape[0])227        matrix_norms = np.linalg.norm(matrix, axis=1)228        matrix_norms[matrix_norms == 0] = 1.0229        return np.dot(matrix, query_vec) / (matrix_norms * query_norm)230
Files · d70086876dc850d656db6f2b5479285154352ae8
Menu

Theme