PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Foswiki/Store/QueryAlgorithms/MongoDB.pm

https://github.com/csirac2/MongoDBPlugin
Perl | 363 lines | 231 code | 59 blank | 73 comment | 31 complexity | 55c5a2dece0bf246e073f7c036d82527 MD5 | raw file
  1. # See the bottom of this file for license and copyright information
  2. =begin TML
  3. ---+ package Foswiki::Store::QueryAlgorithms::MongoDB
  4. Default brute-force query algorithm
  5. Has some basic optimisation: it hoists regular expressions out of the
  6. query to use with grep, so we can narrow down the set of topics that we
  7. have to evaluate the query on.
  8. Not sure exactly where the breakpoint is between the
  9. costs of hoisting and the advantages of hoisting. Benchmarks suggest
  10. that it's around 6 topics, though this may vary depending on disk
  11. speed and memory size. It also depends on the complexity of the query.
  12. =cut
  13. package Foswiki::Store::QueryAlgorithms::MongoDB;
  14. use Foswiki::Store::Interfaces::QueryAlgorithm ();
  15. our @ISA = ('Foswiki::Store::Interfaces::QueryAlgorithm');
  16. use strict;
  17. use constant MONITOR => 0;
  18. BEGIN {
  19. #enable the MongoDBPlugin which keeps the mongodb uptodate with topics changes onsave
  20. #TODO: make conditional - or figure out how to force this in the MongoDB search and query algo's
  21. $Foswiki::cfg{Plugins}{MongoDBPlugin}{Module} =
  22. 'Foswiki::Plugins::MongoDBPlugin';
  23. $Foswiki::cfg{Plugins}{MongoDBPlugin}{Enabled} = 1;
  24. print STDERR "****** starting MongoDBPlugin..\n" if MONITOR;
  25. $Foswiki::Plugins::SESSION->{store}->setListenerPriority('Foswiki::Plugins::MongoDBPlugin::Listener', 1);
  26. }
  27. use Foswiki::Search::Node ();
  28. use Foswiki::Store::SearchAlgorithms::MongoDB();
  29. use Foswiki::Plugins::MongoDBPlugin ();
  30. use Foswiki::Plugins::MongoDBPlugin::Meta ();
  31. use Foswiki::Search::InfoCache;
  32. use Foswiki::Plugins::MongoDBPlugin::HoistMongoDB;
  33. use Data::Dumper;
  34. use Assert;
  35. use Foswiki::Query::Node;
  36. use Foswiki::Query::OP_and;
  37. use Foswiki::Infix::Error;
  38. =begin TML
  39. ---++ ClassMethod new( $class, ) -> $cereal
  40. =cut
  41. sub new {
  42. my $self = shift()->SUPER::new( 'SEARCH', @_ );
  43. return $self;
  44. }
  45. # Query over a single web
  46. sub _webQuery {
  47. my ( $this, $query, $web, $inputTopicSet, $session, $options ) = @_;
  48. #TODO: what happens if / when the inputTopicSet exists?
  49. #presuming that the inputTopicSet is not yet defined, we need to add the topics=, excludetopic= and web options to the query.
  50. my $extra_query;
  51. {
  52. my @option_query = ();
  53. if ( $options->{topic} ) {
  54. push( @option_query,
  55. convertTopicPatternToLonghandQuery( $options->{topic} ) );
  56. }
  57. if ( $options->{excludetopic} ) {
  58. push(
  59. @option_query,
  60. 'NOT('
  61. . convertTopicPatternToLonghandQuery(
  62. $options->{excludetopic}
  63. )
  64. . ')'
  65. );
  66. #> db.current.find({_web: 'Sandbox', _topic : {'$nin' :[ /AjaxComment/]}}, {_topic:1})
  67. #> db.current.find({_web: 'Sandbox', _topic : {'$nin' :[/Web.*/]}}, {_topic:1})
  68. }
  69. my $queryStr = join( ' AND ', @option_query );
  70. #print STDERR "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN($queryStr)\n";
  71. if ( $queryStr eq '' ) {
  72. }
  73. else {
  74. my $theParser = $session->search->{queryParser};
  75. $extra_query = $theParser->parse( $queryStr, $options );
  76. }
  77. }
  78. #SMELL: initialise the mongoDB hack. needed if the mondoPlugin is not enabled, but the algo is selected :/
  79. Foswiki::Plugins::MongoDBPlugin::getMongoDB();
  80. if ( $query->evaluatesToConstant() ) {
  81. # SMELL: use any old topic
  82. my $cache = $Foswiki::Plugins::SESSION->search->metacache->get( $web,
  83. 'WebPreferences' );
  84. my $meta = $cache->{tom};
  85. my $queryIsAConstantFastpath =
  86. $query->evaluate( tom => $meta, data => $meta );
  87. if ( not $queryIsAConstantFastpath ) {
  88. #false - return an empty resultset
  89. return new Foswiki::Search::InfoCache( $Foswiki::Plugins::SESSION,
  90. $web );
  91. }
  92. else {
  93. #need to do the query - at least to eval topic= and excludetopic= and order=
  94. $query = $extra_query;
  95. }
  96. }
  97. else {
  98. if ( defined($extra_query) ) {
  99. my $and = new Foswiki::Query::OP_and();
  100. $query =
  101. Foswiki::Query::Node->newNode( $and, ( $extra_query, $query ) );
  102. }
  103. }
  104. print STDERR "modified parsetree: "
  105. . ( defined($query) ? $query->stringify() : 'undef' ) . "\n"
  106. if MONITOR;
  107. #try HoistMongoDB first
  108. my $mongoQuery =
  109. Foswiki::Plugins::MongoDBPlugin::HoistMongoDB::hoist($query);
  110. if ( not defined($mongoQuery) ) {
  111. print STDERR "MongoDB QuerySearch - failed to hoist to MongoDB ("
  112. . $query->stringify()
  113. . ") - please report the error to Sven.\n";
  114. #falling through to old regex code
  115. }
  116. else {
  117. ASSERT( not( defined( $mongoQuery->{ERROR} ) ) ) if DEBUG;
  118. if (not $session->{users}->isAdmin( $session->{user} )) {
  119. #add ACL filter
  120. my $userIsIn = Foswiki::Plugins::MongoDBPlugin::getACLProfilesFor($session->{user}, $web, $session);
  121. ### ((_ACLProfile_ALLOWTOPICVIEW: $in(userIsIn, UNDEF)) AND (_ACLProfile.DENYTOPICVIEW: $NOTin(userIsIn)))
  122. #TODO: this is incorrect, it needs to also have the logic for the web default (and be inverted if the web DENYs the user..
  123. if ($session->access->haveAccess('VIEW', $session->{user}, $web)) {
  124. #TODO: potential BUG - if user is in both allow and deny, the algo chooses allow
  125. $mongoQuery->{_ACLProfile_ALLOWTOPICVIEW} = {'$in' => [@$userIsIn, 'UNDEFINED']};
  126. $mongoQuery->{_ACLProfile_DENYTOPICVIEW} = {'$nin' => $userIsIn};
  127. } else {
  128. #user is already denied, so we only get view access _if_ the user is specifically ALLOWed
  129. $mongoQuery->{_ACLProfile_ALLOWTOPICVIEW} = {'$in' => [@$userIsIn]};
  130. }
  131. }
  132. #limit, skip, sort_by
  133. my $SortDirection = Foswiki::isTrue( $options->{reverse} ) ? -1 : 1;
  134. #ME bets casesensitive Sorting has no unit tests..
  135. #order="topic"
  136. #order="created"
  137. #order="modified"
  138. #order="editby"
  139. #order="formfield(name)"
  140. #reverse="on"
  141. my %sortKeys = (
  142. topic => '_topic',
  143. #created => , #TODO: don't yet have topic histories in mongo
  144. modified => 'TOPICINFO.date',
  145. editby => 'TOPICINFO.author',
  146. );
  147. my $queryAttrs = {};
  148. my $orderBy = $sortKeys{ $options->{order} || 'topic' };
  149. if ( defined($orderBy) ) {
  150. $queryAttrs = { sort_by => { $orderBy => $SortDirection } };
  151. }
  152. else {
  153. if ( $options->{order} =~ /formfield\((.*)\)/ ) {
  154. #TODO: this will crash things - I need to work on indexes, and one collection per web/form_def
  155. if (
  156. defined(
  157. $Foswiki::cfg{Plugins}{MongoDBPlugin}{ExperimentalCode}
  158. )
  159. and $Foswiki::cfg{Plugins}{MongoDBPlugin}{ExperimentalCode}
  160. )
  161. {
  162. $orderBy = 'FIELD.' . $1 . '.value';
  163. $queryAttrs = { sort_by => { $orderBy => $SortDirection } };
  164. }
  165. }
  166. }
  167. #if ($options->{paging_on}) {
  168. # $queryAttrs->{skip} = $options->{showpage} * $options->{pagesize};
  169. # $queryAttrs->{limit} = $options->{pagesize};
  170. #}
  171. my $cursor = doMongoSearch( $web, $options, $mongoQuery, $queryAttrs );
  172. return new Foswiki::Search::MongoDBInfoCache(
  173. $Foswiki::Plugins::SESSION,
  174. $web, $options, $cursor );
  175. }
  176. ######################################
  177. #fall back to HoistRe
  178. my $topicSet = $inputTopicSet;
  179. if ( !defined($topicSet) ) {
  180. #then we start with the whole web?
  181. #TODO: i'm sure that is a flawed assumption
  182. my $webObject = Foswiki::Meta->new( $session, $web );
  183. $topicSet =
  184. Foswiki::Search::InfoCache::getTopicListIterator( $webObject,
  185. $options );
  186. }
  187. require Foswiki::Query::HoistREs;
  188. my $hoistedREs = Foswiki::Query::HoistREs::hoist($query);
  189. if ( ( !defined( $options->{topic} ) )
  190. and ( $hoistedREs->{name} ) )
  191. {
  192. #set the ' includetopic ' matcher..
  193. #dammit, i have to de-regex it? thats mad.
  194. }
  195. #TODO: howto ask iterator for list length?
  196. #TODO: once the inputTopicSet isa ResultSet we might have an idea
  197. # if ( scalar(@$topics) > 6 ) {
  198. if ( defined( $hoistedREs->{text} ) ) {
  199. my $searchOptions = {
  200. type => ' regex ',
  201. casesensitive => 1,
  202. files_without_match => 1,
  203. };
  204. my @filter = @{ $hoistedREs->{text} };
  205. my $searchQuery =
  206. new Foswiki::Search::Node( $query->toString(), \@filter,
  207. $searchOptions );
  208. $topicSet->reset();
  209. #for now we're kicking down to regex to reduce the set we then brute force query .
  210. #next itr we start to HoistMongoDB
  211. $topicSet =
  212. Foswiki::Store::SearchAlgorithms::MongoDB::_webQuery( $searchQuery,
  213. $web, $topicSet, $session, $searchOptions );
  214. }
  215. else {
  216. #TODO: clearly _this_ can be re-written as a FilterIterator, and if we are able to use the sorting hints (ie DB Store) can propogate all the way to FORMAT
  217. # print STDERR "WARNING: couldn't hoistREs on " . Dumper($query);
  218. }
  219. #print STDERR "))))".$query->toString()."((((\n";
  220. # print STDERR "--------Query::MongoDB \n" . Dumper($query) . "\n";
  221. my $resultTopicSet =
  222. new Foswiki::Search::InfoCache( $Foswiki::Plugins::SESSION, $web );
  223. local $/;
  224. while ( $topicSet->hasNext() ) {
  225. my $webtopic = $topicSet->next();
  226. my ( $Iweb, $topic ) =
  227. Foswiki::Func::normalizeWebTopicName( $web, $webtopic );
  228. #my $meta = Foswiki::Meta->new( $session, $web, $topic );
  229. #GRIN: curiously quick hack to use the MongoDB topics rather than from disk - should have no positive effect on performance :)
  230. #TODO: will make a Store backend later.
  231. my $meta =
  232. Foswiki::Plugins::MongoDBPlugin::Meta->new( $session, $web, $topic );
  233. # this 'lazy load' will become useful when @$topics becomes
  234. # an infoCache
  235. # SMELL: CDot modified this without really understanding how
  236. # it's supposed to work. Once loaded, Meta objects are locked to
  237. # a specific revision of the topic; it's not clear if the metacache
  238. # is intended to include different revisions of the same topic
  239. # or not. See BruteForce.pm for analagous code.
  240. $meta->loadVersion() unless ( $meta->getLoadedRev() );
  241. print STDERR "Processing $topic\n"
  242. if ( Foswiki::Query::Node::MONITOR_EVAL() );
  243. next unless ( $meta->getLoadedRev() );
  244. my $match = $query->evaluate( tom => $meta, data => $meta );
  245. if ($match) {
  246. $resultTopicSet->addTopic($meta);
  247. }
  248. }
  249. return $resultTopicSet;
  250. }
  251. sub doMongoSearch {
  252. my $web = shift;
  253. my $options = shift;
  254. my $ixhQuery = shift;
  255. my $queryAttrs = shift;
  256. #print STDERR "######## Query::MongoDB search ($web) \n";
  257. #print STDERR "querying mongo: "
  258. # . Dumper($ixhQuery) . " , "
  259. # . Dumper($queryAttrs) . "\n";
  260. my $cursor = Foswiki::Plugins::MongoDBPlugin::getMongoDB()
  261. ->query( $web, 'current', $ixhQuery, $queryAttrs );
  262. return $cursor;
  263. }
  264. sub convertTopicPatternToLonghandQuery {
  265. my ($topic) = @_;
  266. return '' unless ($topic);
  267. # 'Web*, FooBar' ==> ( 'Web*', 'FooBar' ) ==> ( 'Web.*', "FooBar" )
  268. my @arr =
  269. map { s/[^\*\_\-\+$Foswiki::regex{mixedAlphaNum}]//go; s/\*/\.\*/go; $_ }
  270. split( /(?:,\s*|\|)/, $topic );
  271. return '' unless (@arr);
  272. # ( 'Web.*', 'FooBar' ) ==> "^(Web.*|FooBar)$"
  273. #return '^(' . join( '|', @arr ) . ')$';
  274. return join(
  275. ' OR ',
  276. map {
  277. if (/\.\*/)
  278. {
  279. "name =~ '" . $_ . "'";
  280. }
  281. else {
  282. "name='" . $_ . "'";
  283. }
  284. } @arr
  285. );
  286. }
  287. 1;
  288. __END__
  289. This copyright information applies to the MongoDBPlugin:
  290. # Plugin for Foswiki - The Free and Open Source Wiki, http://foswiki.org/
  291. #
  292. # Copyright 2010-2011 - SvenDowideit@fosiki.com
  293. #
  294. # MongoDBPlugin is # This program is distributed in the hope that it will be useful,
  295. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  296. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  297. #
  298. # For licensing info read LICENSE file in the root of this distribution.