packages feed

hackage-server-0.5.0: Distribution/Server/Features/Search.hs

{-# LANGUAGE RankNTypes, NamedFieldPuns, RecordWildCards, DoRec #-}
module Distribution.Server.Features.Search (
    SearchFeature(..),
    initSearchFeature,

    -- * Search parameters
    defaultSearchRankParameters,
    SearchEngine.SearchRankParameters(..),
    PkgDocField, PkgDocFeatures,
    BM25F.Explanation(..),
  ) where

import Distribution.Server.Framework
import Distribution.Server.Framework.Templating

import Distribution.Server.Features.Core
import Distribution.Server.Features.PackageList

import Distribution.Server.Features.Search.PkgSearch
import Distribution.Server.Features.Search.SearchEngine (SearchRankParameters(..))
import qualified Distribution.Server.Features.Search.SearchEngine as SearchEngine
import qualified Distribution.Server.Features.Search.BM25F as BM25F
import qualified Distribution.Server.Packages.PackageIndex as PackageIndex

import Distribution.Server.Packages.Types

import Distribution.Package
import Distribution.PackageDescription.Configuration (flattenPackageDescription)

import qualified Data.Text as T
import qualified Data.Map as Map


data SearchFeature = SearchFeature {
    searchFeatureInterface :: HackageFeature,

    searchPackagesResource :: Resource,

    searchPackages        :: MonadIO m => [String] -> m [PackageName],
    searchPackagesExplain :: MonadIO m
                          => SearchRankParameters PkgDocField PkgDocFeatures
                          -> [String]
                          -> m [(BM25F.Explanation PkgDocField PkgDocFeatures T.Text
                                ,PackageName)]
}

instance IsHackageFeature SearchFeature where
    getFeatureInterface = searchFeatureInterface


initSearchFeature :: ServerEnv
                  -> IO (CoreFeature
                      -> ListFeature
                      -> IO SearchFeature)
initSearchFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode} = do
    templates <- loadTemplates serverTemplatesMode
                   [serverTemplatesDir, serverTemplatesDir </> "Search"]
                   [ "opensearch.xml"]

    searchEngineState <- newMemStateWHNF initialPkgSearchEngine

    return $ \core@CoreFeature{..} list -> do
      let feature = searchFeature env core list
                                  searchEngineState templates

      return feature


searchFeature :: ServerEnv
              -> CoreFeature
              -> ListFeature
              -> MemState PkgSearchEngine
              -> Templates
              -> SearchFeature

searchFeature ServerEnv{serverBaseURI} CoreFeature{..} ListFeature{getAllLists}
              searchEngineState templates
  = SearchFeature{..}
  where
    searchFeatureInterface = (emptyHackageFeature "search") {
        featureResources =
          [ searchOpenSearchResource
          , searchPackagesResource
--          , searchSuggestResource
          ]
      , featureState  = []
      , featureCaches = [
            CacheComponent {
              cacheDesc       = "package search engine",
              getCacheMemSize = memSize <$> readMemState searchEngineState
            }
          ]
      , featurePostInit = postInit
      , featureReloadFiles = reloadTemplates templates
      }

    searchOpenSearchResource = (resourceAt "/packages/opensearch.xml") {
        resourceDesc = [(GET, "An OpenSearch description of the package search")],
        resourceGet  = [("xml", handlerGetOpenSearch)]
      }
    -- /packages/search?terms=happstack
    searchPackagesResource = (resourceAt "/packages/search.:format") {
        resourceDesc = [(GET, "Search for packages matching query terms")]
--        resourceGet  = [("json", handlerGetOpenSearch)]
      }

--  searchSuggestResource = (resourceAt "/packages/suggest.:format") {
--      resourceDesc = [(GET, "An OpenSearch description of the package search")]
--      resourceGet = [("json", \_ -> suggestJson)]
--    }

    getSearchDoc = flattenPackageDescription . pkgDesc

    postInit = do
      pkgindex     <- queryGetPackageIndex
      pkgdownloads <- getDownloadCounts
      let pkgs = [ (getSearchDoc pkgLatestVer, pkgdownloads pkgname)
                 | pkgVers <- PackageIndex.allPackagesByName pkgindex
                 , let pkgLatestVer = last pkgVers
                       pkgname      = packageName pkgLatestVer ]
          se = SearchEngine.insertDocs pkgs initialPkgSearchEngine
      writeMemState searchEngineState se

      registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, _) ->
        updatePackage (packageName pkgid)

    --TODO: update periodically for download count changes
    updatePackage :: PackageName -> IO ()
    updatePackage pkgname = do
      index <- queryGetPackageIndex
      let pkgs = PackageIndex.lookupPackageName index pkgname
      case reverse pkgs of
         []      -> modifyMemState searchEngineState
                      (SearchEngine.deleteDoc pkgname)
         (pkg:_) -> do downloads <- getDownloadCount pkgname
                       modifyMemState searchEngineState
                         (SearchEngine.insertDoc (getSearchDoc pkg, downloads))

    getDownloadCount :: PackageName -> IO Int
    getDownloadCount pkgname = do
      pkginfomap <- getAllLists
      return $ maybe 0 itemDownloads (Map.lookup pkgname pkginfomap)

    getDownloadCounts :: IO (PackageName -> Int)
    getDownloadCounts = do
      pkginfomap <- getAllLists
      return (\pkgname -> maybe 0 itemDownloads (Map.lookup pkgname pkginfomap))

    -- Returns list of query results
    searchPackages :: MonadIO m => [String] -> m [PackageName]
    searchPackages terms = do
        se <- readMemState searchEngineState
        let results = SearchEngine.query se (map T.pack terms)
        return results

    searchPackagesExplain :: MonadIO m
                          => SearchRankParameters PkgDocField PkgDocFeatures
                          -> [String]
                          -> m [(BM25F.Explanation PkgDocField PkgDocFeatures T.Text, PackageName)]
    searchPackagesExplain params terms = do
        se <- readMemState searchEngineState
        let results = SearchEngine.queryExplain
                        (SearchEngine.setRankParams params se)
                        (map T.pack terms)
        return results

    handlerGetOpenSearch :: DynamicPath -> ServerPartE Response
    handlerGetOpenSearch _ = do
      template <- getTemplate templates "opensearch.xml"
      let xmlstr = renderTemplate (template ["serverhost" $= show serverBaseURI])
      return $ toResponse (OpenSearchXml xmlstr)

{-
    suggestJson :: ServerPartE Response
    suggestJson =
    --TODO: open search supports a suggest / autocomplete system
-}