"""Indexer — batch-indexes existing Odoo records into the vector store.""" import logging _logger = logging.getLogger(__name__) MODEL_CONFIG = [ { 'model': 'op.course', 'content_type': 'course', 'text_field': 'name', 'description_field': 'description', 'metadata_fields': [], }, { 'model': 'encoach.resource', 'content_type': 'resource', 'text_field': 'name', 'description_field': 'content', 'metadata_fields': ['type', 'cefr_level', 'difficulty'], }, { 'model': 'encoach.question', 'content_type': 'question', 'text_field': 'name', 'description_field': None, 'metadata_fields': ['question_type', 'difficulty', 'skill'], }, { 'model': 'encoach.course.module', 'content_type': 'module', 'text_field': 'name', 'description_field': 'description', 'metadata_fields': ['skill'], }, { 'model': 'encoach.ai.generation.log', 'content_type': 'generation_log', 'text_field': 'brief', 'description_field': 'generated_content', 'metadata_fields': ['course_type', 'status'], }, { 'model': 'encoach.chapter.material', 'content_type': 'material', 'text_field': 'name', 'description_field': 'description', 'metadata_fields': ['type'], }, ] def _get_text(record, config): """Extract indexable text from a record.""" parts = [] text_field = config.get('text_field', 'name') if hasattr(record, text_field): val = getattr(record, text_field) if val: parts.append(str(val)) desc_field = config.get('description_field') if desc_field and hasattr(record, desc_field): val = getattr(record, desc_field) if val: parts.append(str(val)[:2000]) return ' '.join(parts).strip() def _get_metadata(record, config): """Extract metadata dict from a record.""" meta = {} for f in config.get('metadata_fields', []): if hasattr(record, f): val = getattr(record, f) if val: meta[f] = str(val) if not isinstance(val, (int, float, bool)) else val return meta def index_model(env, config, batch_size=100): """Index all records of a single model.""" model_name = config['model'] Model = env.get(model_name) if Model is None: _logger.warning("Model %s not found, skipping", model_name) return 0 Model = Model.sudo() from .embedding_service import EmbeddingService svc = EmbeddingService(env) total = Model.search_count([]) indexed = 0 offset = 0 while offset < total: records = Model.search([], limit=batch_size, offset=offset, order='id') batch_data = [] for rec in records: text = _get_text(rec, config) if text: batch_data.append({ 'id': rec.id, 'text': text, 'metadata': _get_metadata(rec, config), }) if batch_data: indexed += svc.bulk_index(config['content_type'], batch_data) offset += batch_size env.cr.commit() _logger.info("Indexed %d/%d records for %s", indexed, total, model_name) return indexed def index_all(env, batch_size=100): """Index all configured models.""" total = 0 for config in MODEL_CONFIG: try: total += index_model(env, config, batch_size) except Exception: _logger.exception("Failed to index %s", config['model']) _logger.info("Total records indexed: %d", total) return total def cron_reindex(env): """Cron entry point for periodic re-indexing.""" _logger.info("Starting scheduled vector re-index") return index_all(env)