/** @file * @brief Cluster API */ /* Copyright (C) 2010 Richard Boulton * Copyright (C) 2016 Richhiey Thomas * Copyright (C) 2018 Uppinder Chugh * Copyright (C) 2024,2026 Olly Betts * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see * . */ #ifndef XAPIAN_INCLUDED_CLUSTER_H #define XAPIAN_INCLUDED_CLUSTER_H #if !defined XAPIAN_IN_XAPIAN_H && !defined XAPIAN_LIB_BUILD #error Never use directly; include instead. #endif #include #include #include #include #include #include #include #include #include namespace Xapian { /** Stopper subclass which checks for both stemmed and unstemmed stopwords. * * This is intended for use with Xapian::Cluster. */ class XAPIAN_VISIBILITY_DEFAULT StemStopper : public Xapian::Stopper { public: /// Stemming strategies typedef enum { STEM_NONE, STEM_SOME, STEM_ALL, STEM_ALL_Z, STEM_SOME_FULL_POS } stem_strategy; /** Constructor * * @param stemmer The Xapian::Stem object to set. * @param strategy The stemming strategy to be used. */ explicit StemStopper(const Xapian::Stem &stemmer, stem_strategy strategy = STEM_SOME); std::string get_description() const override; bool operator()(const std::string& term) const override { return stop_words.find(term) != stop_words.end(); } /// Add a single stop word and its stemmed equivalent void add(std::string_view term); private: stem_strategy stem_action; std::unordered_set stop_words; Xapian::Stem stemmer; }; /** Class representing a set of documents in a cluster */ class XAPIAN_VISIBILITY_DEFAULT DocumentSet { public: class Internal; /// @private @internal Reference counted internals. Xapian::Internal::intrusive_ptr_nonnull internal; /** Copying is allowed. The internals are reference counted, so * copying is cheap. * * @param other The object to copy. */ DocumentSet(const DocumentSet &other); /** Assignment is allowed. The internals are reference counted, * so assignment is cheap. * * @param other The object to copy. */ DocumentSet & operator=(const DocumentSet &other); /** Move constructor. * * @param other The object to move. */ DocumentSet(DocumentSet && other); /** Move assignment operator. * * @param other The object to move. */ DocumentSet & operator=(DocumentSet && other); /// Default constructor DocumentSet(); /// Destructor ~DocumentSet(); /// Return the size of the DocumentSet Xapian::doccount size() const; /// Return the Document in the DocumentSet at index i const Xapian::Document& operator[](Xapian::doccount i) const; /** Add a new Document to the DocumentSet * * @param document Document object that is to be added to the * DocumentSet */ void add_document(const Document &document); }; /** Base class for TermListGroup * Stores and provides terms that are contained in a document and * their respective term frequencies */ class XAPIAN_VISIBILITY_DEFAULT FreqSource : public Xapian::Internal::opt_intrusive_base { /// Don't allow assignment. void operator=(const FreqSource &) = delete; /// Don't allow copying. FreqSource(const FreqSource &) = delete; public: /// Default constructor FreqSource() {} /// Destructor virtual ~FreqSource(); /** Return the term frequency of a particular term 'term' * * @param term The term for which to return the term frequency */ virtual doccount get_termfreq(const std::string& term) const = 0; /// Return the number of documents within the MSet virtual doccount get_doccount() const = 0; /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated FreqSource * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ FreqSource * release() { opt_intrusive_base::release(); return this; } /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated FreqSource * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ const FreqSource * release() const { opt_intrusive_base::release(); return this; } }; /** A class for construction of termlists which store the terms for a * document along with the number of documents it indexes i.e. term * frequency */ class XAPIAN_VISIBILITY_DEFAULT TermListGroup : public FreqSource { /** Map of the terms and its corresponding term frequencies. * The term frequency of a term stands for the number of documents it indexes */ std::unordered_map termfreq; /// Number of documents added to the termlist doccount num_of_documents; /** Add a single document and calculates its corresponding term frequencies * * @param document Adds a document and updates the TermListGroup * based on the terms found in the document * @param stopper Xapian::Stopper object to identify stopwords */ void add_document(const Document &document, const Stopper *stopper = NULL); public: /** Constructor * * @param docs MSet object used to construct the TermListGroup * @param stopper Xapian::Stopper object to identify stopwords */ explicit TermListGroup(const MSet &docs, const Stopper *stopper = NULL); /** Return the number of documents that the term 'term' exists in * * @param term The term for which to return the term frequency */ doccount get_termfreq(const std::string& term) const override; doccount get_doccount() const override; }; /** Abstract class representing a point in the VSM */ class XAPIAN_VISIBILITY_DEFAULT PointType : public Xapian::Internal::opt_intrusive_base { protected: /** Implement a map to store the terms within a document * and their pre-computed TF-IDF weights */ std::unordered_map weights; /// Store the squared magnitude of the PointType double magnitude = 0.0; /** Set the weight 'weight' to the mapping of a term * * @param term Term for which the weight is supposed * to be changed * @param weight The weight to which the mapping of the * term is to be set */ void set_weight(std::string_view term, double weight) { weights[std::string(term)] = weight; } public: /// Default constructor PointType() {} /// Return a TermIterator to the beginning of the termlist TermIterator termlist_begin() const; /// Return a TermIterator to the end of the termlist TermIterator termlist_end() const noexcept { return TermIterator(NULL); } /** Validate whether a certain term exists in the termlist * or not by performing a lookup operation in the existing values * * @param term Term which is to be searched */ bool contains(std::string_view term) const { return weights.find(std::string(term)) != weights.end(); } /** Return the TF-IDF weight associated with a certain term * * @param term Term for which TF-IDF weight is returned */ double get_weight(std::string_view term) const { auto it = weights.find(std::string(term)); return (it == weights.end()) ? 0.0 : it->second; } /** Add the weight 'weight' to the mapping of a term * * @param term Term to which the weight is to be added * @param weight Weight which has to be added to the existing * mapping of the term */ void add_weight(std::string_view term, double weight) { weights[std::string(term)] += weight; } /// Return the pre-computed squared magnitude double get_magnitude() const { return magnitude; } /// Return the size of the termlist Xapian::termcount termlist_size() const { return Xapian::termcount(weights.size()); } /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated PointType * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ PointType * release() { opt_intrusive_base::release(); return this; } /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated PointType * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ const PointType * release() const { opt_intrusive_base::release(); return this; } }; /** Class to represent a document as a point in the Vector Space * Model */ class XAPIAN_VISIBILITY_DEFAULT Point : public PointType { /// The document which is being represented by the Point Document document; public: /** Constructor * Initialise the point with terms and corresponding TF-IDF weights * * @param freqsource FreqSource object which provides the term * frequencies. It is used for TF-IDF weight * calculations * @param document The Document object over which the Point object * will be initialised */ Point(const FreqSource& freqsource, const Document& document); /// Returns the document corresponding to this Point Document get_document() const { return document; } }; /** Class to represent cluster centroids in the vector space */ class XAPIAN_VISIBILITY_DEFAULT Centroid : public PointType { public: /// Default constructor Centroid() { } /** Constructor with Point argument * * @param point Point object to which Centroid object is * initialised. The document vector and the * magnitude are made equal */ explicit Centroid(const Point &point); /** Divide the weight of terms in the centroid by 'size' and * recalculate the magnitude * * @param cluster_size Value by which Centroid document vector is * divided */ void divide(double cluster_size); /// Clear the terms and corresponding values of the centroid void clear() { weights.clear(); } }; /** Class to represents a Cluster which contains Points and Centroid * of the Cluster */ class XAPIAN_VISIBILITY_DEFAULT Cluster { public: class Internal; /// @private @internal Reference counted internals. Xapian::Internal::intrusive_ptr_nonnull internal; /** Copying is allowed. The internals are reference counted, so * copying is cheap. * * @param other The object to copy. */ Cluster(const Cluster &other); /** Assignment is allowed. The internals are reference counted, * so assignment is cheap. * * @param other The object to copy. */ Cluster& operator=(const Cluster &other); /** Move constructor. * * @param other The object to move. */ Cluster(Cluster && other); /** Move assignment operator. * * @param other The object to move. */ Cluster & operator=(Cluster && other); /** Constructor * * @param centroid The centroid of the cluster object is * assigned to 'centroid' */ explicit Cluster(const Centroid ¢roid); /// Default constructor Cluster(); /// Destructor ~Cluster(); /// Return size of the cluster Xapian::doccount size() const; /** Add a document to the Cluster * * @param point The Point object representing the document which * needs to be added to the cluster */ void add_point(const Point &point); /// Clear the cluster weights void clear(); /// Return the point at the given index in the cluster const Point& operator[](Xapian::doccount i) const; /// Return the documents that are contained within the cluster DocumentSet get_documents() const; /// Return the current centroid of the cluster const Centroid& get_centroid() const; /** Set the centroid of the Cluster to 'centroid' * * @param centroid Centroid object for the Cluster */ void set_centroid(const Centroid ¢roid); /** Recalculate the centroid of the Cluster after each iteration * of the KMeans algorithm by taking the mean of all document vectors (Points) * that belong to the Cluster */ void recalculate(); }; /** Class for storing the results returned by the Clusterer */ class XAPIAN_VISIBILITY_DEFAULT ClusterSet { public: class Internal; /// @private @internal Reference counted internals. Xapian::Internal::intrusive_ptr_nonnull internal; /** Copying is allowed. The internals are reference counted, so * copying is cheap. * * @param other The object to copy. */ ClusterSet(const ClusterSet &other); /** Assignment is allowed. The internals are reference counted, * so assignment is cheap. * * @param other The object to copy. */ ClusterSet& operator=(const ClusterSet &other); /** Move constructor. * * @param other The object to move. */ ClusterSet(ClusterSet && other); /** Move assignment operator. * * @param other The object to move. */ ClusterSet & operator=(ClusterSet && other); /// Default constructor ClusterSet(); /// Destructor ~ClusterSet(); /** Add a cluster to the ClusterSet * * @param cluster Cluster object which is to be added to the ClusterSet */ void add_cluster(const Cluster &cluster); /** Add the point to the cluster at position 'index' * * @param point Point object which needs to be added to * a Cluster within the ClusterSet * @param index Index of the Cluster within the ClusterSet to * which the Point is to be added */ void add_to_cluster(const Point &point, unsigned int index); /// Return the number of clusters Xapian::doccount size() const; /// Return the cluster at index 'i' const Cluster& operator[](Xapian::doccount i) const; /// Clear all the clusters in the ClusterSet void clear_clusters(); /** Recalculate the centroid for all the clusters in the ClusterSet */ void recalculate_centroids(); }; /** Base class for calculating the similarity between documents */ class XAPIAN_VISIBILITY_DEFAULT Similarity { public: /// Destructor virtual ~Similarity(); /** Calculates the similarity between the two documents * * @param a First point object for distance calculation * @param b Second point object for distance calculation */ virtual double similarity(const PointType &a, const PointType &b) const = 0; /// Returns a string describing the similarity metric being used virtual std::string get_description() const = 0; }; /** Class for calculating the cosine distance between two documents */ class XAPIAN_VISIBILITY_DEFAULT CosineDistance : public Similarity { public: /** Calculates and returns the cosine similarity using the * formula cos(theta) = a.b/(|a|*|b|) */ double similarity(const PointType& a, const PointType& b) const override; /// Return a string describing this object std::string get_description() const override; }; /** Class representing an abstract class for a clusterer to be implemented */ class XAPIAN_VISIBILITY_DEFAULT Clusterer : public Xapian::Internal::opt_intrusive_base { public: /// Destructor virtual ~Clusterer(); /** Implement the required clustering algorithm in the subclass and * and return clustered output as ClusterSet * * @param mset The MSet object which contains the documents to be * clustered */ virtual ClusterSet cluster(const MSet &mset) = 0; /// Returns a string describing the clusterer being used virtual std::string get_description() const = 0; /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated Clusterer * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ Clusterer * release() { opt_intrusive_base::release(); return this; } /** Start reference counting this object. * * You can transfer ownership of a dynamically allocated Clusterer * object to Xapian by calling release() and then passing the object to a * Xapian method. Xapian will arrange to delete the object once it is no * longer required. */ const Clusterer * release() const { opt_intrusive_base::release(); return this; } }; /** Kmeans clusterer: * This clusterer implements the K-Means clustering algorithm */ class XAPIAN_VISIBILITY_DEFAULT KMeans : public Clusterer { /// Contains the initialised points that are to be clustered std::vector points; /// Specifies that the clusterer needs to form 'k' clusters unsigned int k; /// Specifies the maximum number of iterations that KMeans will have unsigned int max_iters; /// Pointer to stopper object for identifying stopwords Xapian::Internal::opt_intrusive_ptr stopper; /** Initialise 'k' clusters by selecting 'k' centroids and assigning * them to different clusters * * @param cset ClusterSet object to be initialised by assigning * centroids to each cluster * @param num_of_points Number of points passed to clusterer */ void initialise_clusters(ClusterSet &cset, Xapian::doccount num_of_points); /** Initialise the Points to be fed into the Clusterer with the MSet object * 'source'. The TF-IDF weights for the documents are calculated and stored * within the Points to be used later during distance calculations * * @param source MSet object containing the documents which will be * used to create document vectors that are represented * as Point objects */ void initialise_points(const MSet &source); public: /** Constructor specifying number of clusters and maximum iterations * * @param k_ Number of required clusters * @param max_iters_ The maximum number of iterations for which KMeans * will run if it doesn't converge */ explicit KMeans(unsigned int k_, unsigned int max_iters_ = 0); /** Implements the KMeans clustering algorithm * * @param mset MSet object containing the documents that are to be * clustered */ ClusterSet cluster(const MSet &mset) override; /** Set the Xapian::Stopper object to be used for identifying stopwords. * * Stopwords are discarded while calculating term frequency for terms. * * @param stop The Stopper object to set (default NULL, which means no * stopwords) */ void set_stopper(const Xapian::Stopper* stop = NULL) { stopper = stop; } /// Return a string describing this object std::string get_description() const override; }; /** LCD clusterer: * This clusterer implements the LCD clustering algorithm adapted from * Modelling efficient novelty-based search result diversification in metric * spaces Gil-Costa et al. 2013 */ class XAPIAN_VISIBILITY_DEFAULT LCDClusterer : public Clusterer { /// Specifies that the clusterer needs to form 'k' clusters unsigned int k; public: /** Constructor specifying number of clusters * * @param k_ Number of required clusters */ explicit LCDClusterer(unsigned int k_); /** Implements the LCD clustering algorithm * * @param mset MSet object containing the documents that are to * be clustered */ ClusterSet cluster(const MSet &mset) override; /// Return a string describing this object std::string get_description() const override; }; } #endif // XAPIAN_INCLUDED_CLUSTER_H