//go:build xapian package index /* #cgo pkg-config: xapian-core #cgo LDFLAGS: -lstdc++ #include "xapian_wrapper.h" #include */ import "C" import ( "encoding/json" "fmt" "unsafe" ) type xapianIndex struct { db *C.XapianDB } func newXapian(dir string) (Indexer, error) { cdir := C.CString(dir) defer C.free(unsafe.Pointer(cdir)) var cerr *C.char db := C.xapian_open(cdir, 1, &cerr) if db == nil { msg := C.GoString(cerr) C.xapian_free_string(cerr) return nil, fmt.Errorf("xapian open: %s", msg) } return &xapianIndex{db: db}, nil } func (x *xapianIndex) IndexSync(doc MailDocument) error { cid := C.CString(doc.ID) defer C.free(unsafe.Pointer(cid)) cfrom := C.CString(doc.From) defer C.free(unsafe.Pointer(cfrom)) cto := C.CString(doc.To) defer C.free(unsafe.Pointer(cto)) csubj := C.CString(doc.Subject) defer C.free(unsafe.Pointer(csubj)) cbody := C.CString(doc.Body) defer C.free(unsafe.Pointer(cbody)) hasAttach := C.int(0) if doc.HasAttachment { hasAttach = C.int(1) } var cerr *C.char rc := C.xapian_index(x.db, cid, cfrom, cto, csubj, cbody, C.longlong(doc.Date.Unix()), hasAttach, &cerr) if rc != 0 { msg := C.GoString(cerr) C.xapian_free_string(cerr) return fmt.Errorf("xapian index: %s", msg) } return nil } func (x *xapianIndex) Delete(id string) error { cid := C.CString(id) defer C.free(unsafe.Pointer(cid)) var cerr *C.char rc := C.xapian_delete(x.db, cid, &cerr) if rc != 0 { msg := C.GoString(cerr) C.xapian_free_string(cerr) return fmt.Errorf("xapian delete: %s", msg) } return nil } func (x *xapianIndex) Search(req SearchRequest) (*SearchResult, error) { cquery := C.CString(req.Query) defer C.free(unsafe.Pointer(cquery)) cfrom := C.CString(req.From) defer C.free(unsafe.Pointer(cfrom)) cown := C.CString(req.OwnEmail) defer C.free(unsafe.Pointer(cown)) cto := C.CString(req.To) defer C.free(unsafe.Pointer(cto)) var dateFrom, dateTo C.longlong if req.DateFrom != nil { dateFrom = C.longlong(req.DateFrom.Unix()) } if req.DateTo != nil { dateTo = C.longlong(req.DateTo.Unix()) } page := req.Page if page < 1 { page = 1 } offset := C.int((page - 1) * req.PageSize) limit := C.int(req.PageSize) if limit <= 0 { limit = 25 } // Sort mode: 0=relevance, 1=date_desc (default), 2=date_asc sortMode := C.int(1) switch req.Sort { case "relevance": sortMode = C.int(0) case "date_asc": sortMode = C.int(2) } // Attachment filter: 0=all, 1=only with, -1=only without attachFilter := C.int(0) if req.HasAttachment != nil { if *req.HasAttachment { attachFilter = C.int(1) } else { attachFilter = C.int(-1) } } var cerr *C.char cresult := C.xapian_search(x.db, cquery, cfrom, cown, cto, dateFrom, dateTo, offset, limit, sortMode, attachFilter, &cerr) if cresult == nil { msg := C.GoString(cerr) C.xapian_free_string(cerr) return nil, fmt.Errorf("xapian search: %s", msg) } defer C.xapian_free_string(cresult) jsonStr := C.GoString(cresult) var raw struct { Total int `json:"total"` Hits []struct { ID string `json:"id"` Score float64 `json:"score"` } `json:"hits"` } if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { return nil, fmt.Errorf("xapian parse result: %w", err) } hits := make([]Hit, len(raw.Hits)) for i, h := range raw.Hits { hits[i] = Hit{ID: h.ID, Score: h.Score} } return &SearchResult{Total: raw.Total, Hits: hits}, nil } func (x *xapianIndex) Close() error { C.xapian_close(x.db) return nil }