Ë
    UKj¥$  ã                  óP   — d Z ddlmZ ddlZddlZddlmZ ddlmZ  G d„ d«      Z	y)z2SQLite + FTS5 catalog index for fast offer lookup.é    )ÚannotationsN)ÚPath)ÚOfferc                  óˆ   — e Zd ZdZddd„Zdd„Zdd„Zdd„Zdd„Zdd„Z	dd„Z
dd	„Z	 	 	 d	 	 	 	 	 	 	 dd„Zdd„Zdd„Zdd„Zy
)ÚCatalogIndexz5Local index of seller offers backed by SQLite + FTS5.c                óì   — |dk7  r&t        |«      j                  j                  dd¬«       t        j                  |«      | _        t        j                  | j
                  _        | j                  «        y )Nú:memory:T)ÚparentsÚexist_ok)	r   ÚparentÚmkdirÚsqlite3ÚconnectÚconnÚRowÚrow_factoryÚ_init_schema)ÚselfÚdb_paths     úF/var/www/html/agent.samochodowy/src/agent_samochodowy/matcher/index.pyÚ__init__zCatalogIndex.__init__   sS   € Ø�jÒ Ü�‹M× Ñ ×&Ñ&¨t¸dÐ&ÔCÜ—O‘O GÓ,ˆŒ	Ü '§¡ˆ�	‰	ÔØ×ÑÕó    c                ó°   — | j                   j                  «       }|j                  d«       |j                  d«       | j                   j	                  «        y )NaE  
            CREATE TABLE IF NOT EXISTS offers (
                offer_id TEXT PRIMARY KEY,
                platform TEXT NOT NULL,
                url TEXT NOT NULL,
                title TEXT NOT NULL,
                price REAL,
                currency TEXT DEFAULT 'PLN',
                qty INTEGER DEFAULT 1,
                brand TEXT,
                model TEXT,
                category TEXT,
                engine_code TEXT,
                oe_numbers_json TEXT DEFAULT '[]',
                is_part INTEGER DEFAULT 1
            );

            CREATE TABLE IF NOT EXISTS oe_index (
                oe_number TEXT NOT NULL,
                offer_id TEXT NOT NULL,
                UNIQUE(oe_number, offer_id)
            );
            CREATE INDEX IF NOT EXISTS idx_oe ON oe_index(oe_number);

            CREATE TABLE IF NOT EXISTS engine_index (
                engine_code TEXT NOT NULL,
                offer_id TEXT NOT NULL,
                UNIQUE(engine_code, offer_id)
            );
            CREATE INDEX IF NOT EXISTS idx_engine ON engine_index(engine_code);
        zæ
            CREATE VIRTUAL TABLE IF NOT EXISTS offers_fts USING fts5(
                offer_id UNINDEXED, title, category, brand, model, engine_code,
                tokenize='unicode61 remove_diacritics 2'
            )
        )r   ÚcursorÚexecutescriptÚexecuteÚcommit©r   Úcurs     r   r   zCatalogIndex._init_schema   sM   € Ø�i‰i×ÑÓ ˆØ×Ñð ô 	ð@ 	�‰ð ô 	ð 	�	‰	×ÑÕr   c                ó@  — | j                   j                  «       }t        j                  |j                  «      }|j                  d|j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                   |t#        |j$                  «      f«       |j                  d|j                  f«       |j                  D ]   }|j                  d||j                  f«       Œ" |j                  d|j                  f«       |j                   r6|j                  d|j                   j'                  «       |j                  f«       | j                   j)                  «        y)z6Insert or update a single offer and its index entries.a­  
            INSERT INTO offers (offer_id, platform, url, title, price, currency, qty,
                               brand, model, category, engine_code, oe_numbers_json, is_part)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(offer_id) DO UPDATE SET
                platform=excluded.platform, url=excluded.url, title=excluded.title,
                price=excluded.price, currency=excluded.currency, qty=excluded.qty,
                brand=excluded.brand, model=excluded.model, category=excluded.category,
                engine_code=excluded.engine_code, oe_numbers_json=excluded.oe_numbers_json,
                is_part=excluded.is_part
        z'DELETE FROM oe_index WHERE offer_id = ?zBINSERT OR IGNORE INTO oe_index (oe_number, offer_id) VALUES (?, ?)z+DELETE FROM engine_index WHERE offer_id = ?zHINSERT OR IGNORE INTO engine_index (engine_code, offer_id) VALUES (?, ?)N)r   r   ÚjsonÚdumpsÚ
oe_numbersr   Úoffer_idÚplatformÚurlÚtitleÚpriceÚcurrencyÚqtyÚbrandÚmodelÚcategoryÚengine_codeÚintÚis_partÚupperr   )r   Úofferr   Úoe_jsonÚoes        r   Úupsert_offerzCatalogIndex.upsert_offer@   s:  € à�i‰i×ÑÓ ˆÜ—*‘*˜U×-Ñ-Ó.ˆà�‰ð 
ð �N‰N˜EŸN™N¨E¯I©I°u·{±{Ø�K‰K˜Ÿ™¨¯©Ø�K‰K˜Ÿ™ e§n¡nØ×Ñ˜w¬¨E¯M©MÓ(:ð	
ô	ð$ 	�‰Ð=ÀÇÁÐ?PÔQØ×"Ñ"ò 	ˆBØ�K‰KØTØ�U—^‘^Ð$õð	ð 	�‰ÐAÀEÇNÁNÐCTÔUØ×ÒØ�K‰KØZØ×"Ñ"×(Ñ(Ó*¨E¯N©NÐ;ôð
 	�	‰	×ÑÕr   c                ón   — |D ]   }|j                   sŒ| j                  |«       Œ" | j                  «        y)z Bulk-load offers into the index.N)r0   r5   Ú_rebuild_fts)r   Úoffersr2   s      r   Úbuild_from_offerszCatalogIndex.build_from_offersh   s4   € àò 	)ˆEØ�}‹}Ø×!Ñ! %Õ(ð	)ð 	×ÑÕr   c                ó°   — | j                   j                  «       }|j                  d«       |j                  d«       | j                   j                  «        y)z-Rebuild the FTS5 index from the offers table.zDELETE FROM offers_ftszÔ
            INSERT INTO offers_fts(offer_id, title, category, brand, model, engine_code)
            SELECT offer_id, title, category, brand, model, engine_code
            FROM offers WHERE is_part = 1
        N)r   r   r   r   r   s     r   r7   zCatalogIndex._rebuild_ftso   sD   € à�i‰i×ÑÓ ˆØ�‰Ð,Ô-Ø�‰ð ô 	ð
 	�	‰	×ÑÕr   c                óì   — g }|D ]l  }|j                  t        |d   |d   |d   |d   |d   |d   |d   |d   |d	   |d
   |d   t        j                  |d   «      t	        |d   «      ¬«      «       Œn |S )Nr$   r%   r&   r'   r(   r)   r*   r+   r,   r-   r.   Úoe_numbers_jsonr0   )r$   r%   r&   r'   r(   r)   r*   r+   r,   r-   r.   r#   r0   )Úappendr   r!   ÚloadsÚbool)r   ÚrowsÚresultÚrows       r   Ú_rows_to_offerszCatalogIndex._rows_to_offersz   sœ   € ØˆØò 	ˆCØ�M‰Mœ%Ø˜Z™Ø˜Z™Ø˜‘JØ˜'‘lØ˜'‘lØ˜Z™Ø˜‘JØ˜'‘lØ˜'‘lØ˜Z™Ø Ñ.ÜŸ:™: cÐ*;Ñ&<Ó=Ü˜S ™^Ó,ôõ ð	ð  ˆr   c                óš   — | j                   j                  «       }|j                  d|f«       | j                  |j	                  «       «      S )zExact OE number lookup.z™
            SELECT o.* FROM offers o
            JOIN oe_index i ON o.offer_id = i.offer_id
            WHERE i.oe_number = ? AND o.is_part = 1
        )r   r   r   rC   Úfetchall)r   r4   r   s      r   Úsearch_by_oezCatalogIndex.search_by_oeŽ   sE   € à�i‰i×ÑÓ ˆØ�‰ð ð ˆUô		ð
 ×#Ñ# C§L¡L£NÓ3Ð3r   c                ó¶   — | j                   j                  «       }|j                  d|j                  «       f«       | j	                  |j                  «       «      S )zExact engine code lookup.zŸ
            SELECT o.* FROM offers o
            JOIN engine_index i ON o.offer_id = i.offer_id
            WHERE i.engine_code = ? AND o.is_part = 1
        )r   r   r   r1   rC   rE   )r   Úcoder   s      r   Úsearch_by_engine_codez"CatalogIndex.search_by_engine_code˜   sK   € à�i‰i×ÑÓ ˆØ�‰ð ð �j‰j‹lˆ_ô		ð
 ×#Ñ# C§L¡L£NÓ3Ð3r   Nc                óØ  — ddl m} dg}g }|r"|j                  d«       |j                  |«       |r%|j                  d«       |j                  |||g«       |r¥ ||«      }g }|D ](  }	|j                  d«       |j                  d|	› d�«       Œ* |D ]E  }	t	        |	j                  «       «      dk  sŒ |j                  d	«       |j                  d
|	› d�«       ŒG |j                  ddj                  |«      › d�«       t	        |«      dk(  rg S | j                  j                  «       }
|
j                  ddj                  |«      › d�|«       | j                  |
j                  «       «      S )uê   Attribute-based search (brand, model, category).

        Category matching uses synonym expansion: a query for "sprÄ™Å¼arka powietrza"
        also matches offers with category "compressor" or title containing "SPRÄ˜Å»ARKA".
        r   )Úexpand_category_synonymszis_part = 1zLOWER(brand) = LOWER(?)zc(LOWER(model) = LOWER(?) OR LOWER(model) LIKE LOWER(?) || '%' OR LOWER(?) LIKE LOWER(model) || '%')zLOWER(category) LIKE LOWER(?)ú%é   zLOWER(title) LIKE LOWER(?)z% z %ú(ú OR ú)é   zSELECT * FROM offers WHERE z AND z	 LIMIT 50)Ú#agent_samochodowy.matcher.normalizerK   r=   ÚextendÚlenÚsplitÚjoinr   r   r   rC   rE   )r   r+   r,   r-   rK   Ú
conditionsÚparamsÚsynonymsÚcat_clausesÚsynr   s              r   Úsearch_by_attributesz!CatalogIndex.search_by_attributes¢   sm  € õ 	Qà#�_ˆ
ØˆÙØ×ÑÐ7Ô8Ø�M‰M˜%Ô Ùð ×Ñðôð �M‰M˜5 %¨Ð/Ô0ÙÙ/°Ó9ˆHØˆKØò *�Ø×"Ñ"Ð#BÔCØ—‘  #  a˜jÕ)ð*ð  ò 0�Ü�s—y‘y“{Ó# qÓ(Ø×&Ñ&Ð'CÔDØ—M‘M B s e¨2 ,Õ/ð0ð ×Ñ  &§+¡+¨kÓ":Ð!;¸1Ð=Ô>äˆz‹?˜aÒØˆIà�i‰i×ÑÓ ˆØ�‰Ø)¨'¯,©,°zÓ*BÐ)CÀ9ÐMÈvô	
ð ×#Ñ# C§L¡L£NÓ3Ð3r   c                óD  — ddl }|j                  d||j                  «      }|sg S dj                  |dd «      }| j                  j                  «       }	 |j                  d|f«       | j                  |j                  «       «      S # t        j                  $ r g cY S w xY w)z7FTS5 full-text search on title, category, brand, model.r   Nz\w+rO   é
   zÇ
                SELECT o.* FROM offers_fts f
                JOIN offers o ON f.offer_id = o.offer_id
                WHERE offers_fts MATCH ? AND o.is_part = 1
                LIMIT 20
            )ÚreÚfindallÚUNICODErV   r   r   r   rC   rE   r   ÚOperationalError)r   Útextr_   ÚtokensÚ	fts_queryr   s         r   Úsearch_fuzzyzCatalogIndex.search_fuzzyÖ   s›   € ó 	Ø—‘˜F D¨"¯*©*Ó5ˆÙØˆIà—K‘K  s¨ Ó,ˆ	Ø�i‰i×ÑÓ ˆð		Ø�K‰Kð ð
 �ôð ×'Ñ'¨¯©«Ó7Ð7øÜ×'Ñ'ò 	ØŠIð	ús   Á1B ÂBÂBc                ó~   — | j                   j                  «       }|j                  d«       |j                  «       d   S )Nz-SELECT COUNT(*) FROM offers WHERE is_part = 1r   )r   r   r   Úfetchoner   s     r   ÚcountzCatalogIndex.countë   s2   € Ø�i‰i×ÑÓ ˆØ�‰ÐCÔDØ�|‰|‹~˜aÑ Ð r   c                ó8   — | j                   j                  «        y )N)r   Úclose)r   s    r   rk   zCatalogIndex.closeð   s   € Ø�	‰	�‰Õr   )r	   )r   ÚstrÚreturnÚNone)rm   rn   )r2   r   rm   rn   )r8   úlist[Offer]rm   rn   )r@   zlist[sqlite3.Row]rm   ro   )r4   rl   rm   ro   )rH   rl   rm   ro   )NNN)r+   ú
str | Noner,   rp   r-   rp   rm   ro   )rc   rl   rm   ro   )rm   r/   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r   r5   r9   r7   rC   rF   rI   r\   rf   ri   rk   © r   r   r   r      su   „ Ù?ôó(óT&óPó	óó(4ó4ð !Ø Ø#ð	24àð24ð ð24ð ð	24ð
 
ó24óhó*!ô
r   r   )
rt   Ú
__future__r   r!   r   Úpathlibr   Úagent_samochodowy.modelsr   r   ru   r   r   ú<module>ry      s#   ðÙ 8å "ã Û Ý å *÷eò er   