mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-07-25 04:48 am

[dw-free] This is the first pass at the search system for Dreamwidth. For more information on how t

[commit: http://hg.dwscoalition.org/dw-free/rev/4dedfba1fe16]

This is the first pass at the search system for Dreamwidth. For more
information on how to set this up, please see:

http://wiki.dwscoalition.org/notes/Setting_Up_Search

Patch by [staff profile] mark.

Files modified:
  • bin/schedule-copier-jobs
  • bin/upgrading/en.dat
  • bin/worker/sphinx-copier
  • bin/worker/sphinx-search-gm
  • cgi-bin/DW/Logic/ProfilePage.pm
  • cgi-bin/DW/Logic/UserLinkBar.pm
  • cgi-bin/DW/Pay.pm
  • cgi-bin/LJ/Entry.pm
  • cgi-bin/ljprotocol.pl
  • htdocs/img/silk/profile/search.png
  • htdocs/search.bml
--------------------------------------------------------------------------------
diff -r 6865fbf1b5da -r 4dedfba1fe16 bin/schedule-copier-jobs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/schedule-copier-jobs	Sat Jul 25 04:48:46 2009 +0000
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+#
+# bin/schedule-copier-jobs
+#
+# A simple job that schedules copier tasks for the sphinx copier.  This should
+# be run whenever you want to clean up the database of new/old data.  Probably
+# no more often than once every few weeks.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+
+die "Need to have Sphinx search enabled.\n"
+    unless @LJ::SPHINX_SEARCHD;
+
+my $dbr = LJ::get_db_reader() or die;
+my $sth = $dbr->prepare( q{SELECT userid FROM user WHERE journaltype IN ('P','C')} );
+$sth->execute;
+
+my $sclient = LJ::theschwartz() or die;
+
+while ( my ( $userid ) = $sth->fetchrow_array ) {
+    warn "Scheduling $userid ...\n";
+    $sclient->insert_jobs( TheSchwartz::Job->new_from_array( 'DW::Worker::Sphinx::Copier', { userid => $userid } ) );
+}
diff -r 6865fbf1b5da -r 4dedfba1fe16 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Sat Jul 25 04:43:19 2009 +0000
+++ b/bin/upgrading/en.dat	Sat Jul 25 04:48:46 2009 +0000
@@ -3633,6 +3633,10 @@ userlinkbar.postentry=Post an Entry
 
 userlinkbar.postentry.title=Post to your journal
 
+userlinkbar.search=Search for Entries
+
+userlinkbar.search.title=Search for entries in this account
+
 userlinkbar.sendmessage=Send Message
 
 userlinkbar.sendmessage.title=Send a private message to this user
diff -r 6865fbf1b5da -r 4dedfba1fe16 bin/worker/sphinx-copier
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/sphinx-copier	Sat Jul 25 04:48:46 2009 +0000
@@ -0,0 +1,155 @@
+#!/usr/bin/perl
+#
+# bin/worker/sphinx-copier
+#
+# Responsible for ensuring a user is up to date when they make a new post or
+# edit an existing one.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+use LJ::Worker::TheSchwartz;
+
+schwartz_decl( 'DW::Worker::Sphinx::Copier' );
+schwartz_work();
+
+# ============================================================================
+package DW::Worker::Sphinx::Copier;
+use base 'TheSchwartz::Worker';
+use Encode;
+use DW::Pay;
+use LJ::DBUtil;
+
+sub work {
+    my ( $class, $job ) = @_;
+    my $a = $job->arg;
+
+    my $u = LJ::load_userid( $a->{userid} )
+        or die "Invalid userid: $a->{userid}.\n";
+    my $dbto = LJ::get_dbh( 'sphinx_search' )
+        or die "Unable to connect to Sphinx search database.\n";
+    my $p_revtime = LJ::get_prop( log => 'revtime' )
+        or die "No logprop revtime?\n";
+
+    # annotate copier is going
+    warn "[$$] Sphinx copier started for " . $u->user . "(" . $u->id . ").\n";
+
+    # we have to use utf8 when we write to the db
+    $dbto->do( 'SET NAMES \'utf8\'' );
+
+    # if this is a free user, then it's easy
+    my $ps = DW::Pay::get_paid_status( $u );
+    unless ( $ps && ( $ps->{permanent} || $ps->{expiresin} > 0 ) ) {
+        warn "[$$] Removing " . $u->user . "(" . $u->id . ") from the index [free user]...\n";
+        $dbto->do( 'DELETE FROM posts_raw WHERE journal_id = ?', undef, $u->id );
+        die $dbto->errstr if $dbto->err;
+        return $job->completed;
+    }
+
+    # they're a paid user of some sort, so get the posts we know about
+    my $sphinx_times = $dbto->selectall_hashref(
+        'SELECT jitemid, revtime FROM posts_raw WHERE journal_id = ?',
+        'jitemid', undef, $u->id
+    );
+    die $dbto->errstr if $dbto->err;
+
+    # now get the times for all of their posts from the database (inactive if possible)
+    my $dbfrom;
+    if ( $LJ::CLUSTER_PAIR_ACTIVE{$u->clusterid} ) {
+        $dbfrom = LJ::DBUtil->get_inactive_db( $u->clusterid );
+    }
+    $dbfrom ||= LJ::get_cluster_reader( $u->clusterid );
+    die "No dbfrom available.\n"
+        unless $dbfrom;
+
+    # okay, we're actually getting the times now
+    my $db_times = $dbfrom->selectall_hashref(
+        qq{SELECT l.jitemid, UNIX_TIMESTAMP(l.logtime) AS 'createtime', lp.value AS 'edittime'
+           FROM log2 l LEFT JOIN logprop2 lp ON (l.journalid = lp.journalid AND l.jitemid = lp.jitemid AND lp.propid = ?)
+               WHERE l.journalid = ?},
+        'jitemid', undef, $p_revtime->{id}, $u->id
+    );
+    die $dbfrom->errstr if $dbfrom->err;
+
+    # three doors: ignore, copy, delete
+    my ( @copy_jitemids, @delete_jitemids );
+
+    # now iterate through and find entirely NEW and EDITED posts
+    foreach my $jitemid ( keys %$db_times ) {
+
+        # we have to decide which time to use.  we want the edit time if one is available,
+        # but we'll use the createtime if not
+        $db_times->{$jitemid} = $db_times->{$jitemid}->{edittime} || $db_times->{$jitemid}->{createtime};
+        $sphinx_times->{$jitemid} = $sphinx_times->{$jitemid}->{revtime};
+
+        next if exists $sphinx_times->{$jitemid} &&
+                $sphinx_times->{$jitemid} == $db_times->{$jitemid};
+
+        push @copy_jitemids, $jitemid;
+    }
+
+    # now find deleted posts
+    foreach my $jitemid ( keys %$sphinx_times ) {
+        next if exists $db_times->{$jitemid};
+
+        warn "[$$] Deleting post #$jitemid.\n";
+        push @delete_jitemids, $jitemid;
+    }
+
+    # deletes are easy...
+    if ( @delete_jitemids ) {
+        my $ct = $dbto->do( 'DELETE FROM posts_raw WHERE journal_id = ? AND jitemid IN (' .
+                            join( ',', @delete_jitemids ) . ')', undef, $u->id ) + 0;
+        die $dbto->errstr if $dbto->err;
+
+        warn "[$$] Actually deleted $ct posts.\n";
+    }
+
+    # now to copy entries.  this is not done enmasse since the major case will be after a user
+    # already has most of their posts copied and they are just updating one or two.
+    foreach my $jitemid ( @copy_jitemids ) {
+        my $row = $dbfrom->selectrow_hashref(
+            qq{SELECT l.journalid, l.jitemid, l.posterid, l.security, l.eventtime, lt.subject, lt.event
+               FROM log2 l INNER JOIN logtext2 lt ON (l.journalid = lt.journalid AND l.jitemid = lt.jitemid)
+               WHERE l.journalid = ? AND l.jitemid = ?},
+            undef, $u->id, $jitemid
+        );
+        die $dbfrom->errstr if $dbfrom->err;
+
+        # have to do some more munging
+        $row->{is_public} = $row->{security} eq 'public' ? 1 : 0;
+        $row->{edittime} = $db_times->{$jitemid};
+
+        # very important, the search engine can't index compressed crap...
+        foreach ( qw/ subject event / ) {
+            LJ::text_uncompress( \$row->{$_} );
+            $row->{$_} = Encode::decode( 'utf8', $row->{$_} );
+        }
+
+        # insert
+        $dbto->do( 'REPLACE INTO posts_raw (id, journal_id, jitemid, poster_id, is_public, date_posted, title, data, revtime) ' .
+                   'VALUES (NULL, ?, ?, ?, ?, UNIX_TIMESTAMP(?), ?, ?, ?)',
+                   undef, map { $row->{$_} } qw/ journalid jitemid posterid is_public eventtime subject event edittime /, );
+        die $dbto->errstr if $dbto->err;
+
+        # let the viewer know what they missed
+        warn "[$$] Inserted post #$jitemid for " . $u->user . "(" . $u->id . ").\n";
+    }
+
+    # all good
+    return $job->completed;
+}
+
+sub keep_exit_status_for { 0 }
+sub grab_for { 1800 }
+sub max_retries { 3 }
+sub retry_delay { 1800 }
diff -r 6865fbf1b5da -r 4dedfba1fe16 bin/worker/sphinx-search-gm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/sphinx-search-gm	Sat Jul 25 04:48:46 2009 +0000
@@ -0,0 +1,112 @@
+#!/usr/bin/perl
+#
+# sphinx-search-gm
+#
+# This Gearman worker is responsible for taking a search and issuing it to the
+# Sphinx searchd.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+require "ljlib.pl";
+
+use Encode;
+use Gearman::Worker;
+use LJ::Worker::Gearman;
+use Sphinx::Search;
+use Storable;
+
+gearman_decl( 'sphinx_search'  => \&sphinx_search );
+gearman_work();
+
+sub sphinx_search {
+    my $job = $_[0];
+
+    my $args = Storable::thaw( $job->arg ) || {};
+    return undef
+        unless $args->{userid} && $args->{query};
+
+    my $sx = Sphinx::Search->new();
+    $sx->SetServer( @LJ::SPHINX_SEARCHD );
+
+    $sx->SetMatchMode( SPH_MATCH_ALL )
+       ->SetSortMode( SPH_SORT_RELEVANCE )
+       ->SetMaxQueryTime( 15_000 )
+       ->SetFilter( 'journal_id', [ $args->{userid} ] )
+       ->SetLimits( $args->{offset} || 0, 20 );
+
+    $sx->SetFilter( 'is_public', [ 1 ] )
+        if $args->{public};
+
+    my $res = $sx->Query( $args->{query} );
+    return undef unless $res;
+
+    # try to build some excerpts of these searches, which involves us loading
+    # up the exact entry contents...
+    if ( $res->{total} > 0 ) {
+
+        # this is weird, I push the hashrefs onto @out from $res->{matches} for
+        # convenience only... they're the same hashrefs you know and love
+        my @out;
+
+        foreach my $match ( @{ $res->{matches} } ) {
+            my $entry = LJ::Entry->new( $match->{journal_id}, jitemid => $match->{jitemid} );
+            if ( $entry && $entry->valid ) {
+                # use text only version of event for excerpt purposes.  best effort.
+                $match->{entry} = $entry->event_text;
+                $match->{entry} =~ s#<(?:br|p)\s*/?># #gi;
+                $match->{entry} = LJ::strip_html( $match->{entry} );
+
+                # we don't munge the subject... just clean it
+                $match->{subject} = $entry->subject_text || '(no subject)';
+
+                # also useful information that we want for later
+                $match->{url} = $entry->url;
+                $match->{tags} = $entry->tag_map;
+                $match->{security} = $entry->security;
+                $match->{security} = 'access'
+                    if $match->{security} eq 'usemask' &&
+                       $entry->allowmask == 1;
+                $match->{eventtime} = $entry->eventtime_mysql;
+
+            } else {
+                # something happened, couldn't get the entry
+                $match->{entry} = '(sorry, this entry has been deleted or is otherwise unavailable)';
+                $match->{subject} = 'Entry deleted or unavailable.';
+            }
+            push @out, $match;
+        }
+
+        # FIXME: this should use some other index name than 'test1' heh, and we should probably
+        # try to figure out the language of the journal being searched (or the searcher?) and use
+        # an appropriate stemming library?  (future expansion opportunity)
+        my $exc = $sx->BuildExcerpts( [ map { $_->{entry} } @out ], 'test1stemmed', $args->{query}, {} );
+
+        # if we have a matching number of excerpts to events, then we can determine
+        # which one goes with which post.
+        if ( scalar( @out ) == scalar( @$exc ) ) {
+            foreach my $m ( @out ) {
+                delete $m->{entry};
+                $m->{excerpt} = shift @$exc;
+            }
+
+        } else {
+            # something terrible has happened..., user gets no excerpts :(
+            foreach my $m ( @out ) {
+                delete $m->{entry};
+                $m->{excerpt} = '(something terrible happened to the excerpts)';
+            }
+        }
+    }
+
+    return Storable::nfreeze( $res );
+}
diff -r 6865fbf1b5da -r 4dedfba1fe16 cgi-bin/DW/Logic/ProfilePage.pm
--- a/cgi-bin/DW/Logic/ProfilePage.pm	Sat Jul 25 04:43:19 2009 +0000
+++ b/cgi-bin/DW/Logic/ProfilePage.pm	Sat Jul 25 04:48:46 2009 +0000
@@ -58,7 +58,7 @@ sub action_links {
     my $remote = $self->{remote};
     
     my $user_link_bar = $u->user_link_bar( $remote, class_prefix => "profile" );
-    my @ret = $user_link_bar->get_links( "manage_membership", "trust", "watch", "post", "track", "message", "buyaccount" );
+    my @ret = $user_link_bar->get_links( "manage_membership", "trust", "watch", "post", "track", "message", "search", "buyaccount" );
 }
 
 
diff -r 6865fbf1b5da -r 4dedfba1fe16 cgi-bin/DW/Logic/UserLinkBar.pm
--- a/cgi-bin/DW/Logic/UserLinkBar.pm	Sat Jul 25 04:43:19 2009 +0000
+++ b/cgi-bin/DW/Logic/UserLinkBar.pm	Sat Jul 25 04:48:46 2009 +0000
@@ -38,6 +38,7 @@ DW::Logic::UserLinkBar - This module pro
   $link = $user_link_bar->message;
   $link = $user_link_bar->tellafriend;
   $link = $user_link_bar->memories;
+  $link = $user_link_bar->search;
 
 =cut
 
@@ -453,6 +454,36 @@ sub memories {
     return $self->fix_link( $link );
 }
 
+=head2 C<< $obj->search >>
+
+Returns a hashref with the appropriate icon/link/text for searching this journal.
+
+=cut
+
+sub search {
+    my $self = $_[0];
+
+    my $u = $self->{u};
+    my $remote = $self->{remote};
+    my $user = $u->user;
+
+    # don't show if search is disabled
+    return undef unless
+        @LJ::SPHINX_SEARCHD &&
+        ( $u->is_community || ( $u->equals( $remote ) ) ) &&
+        $u->is_paid;
+
+    my $link = {
+        url => 'search' . ( $u->is_community ? "?search_user=" . $u->user : '' ),
+        image => 'search.png',
+        text_ml => "userlinkbar.search",
+        title_ml => "userlinkbar.search.title",
+        class => 'search',
+    };
+
+    return $self->fix_link( $link );
+}
+
 =head2 C<< $obj->buyaccount >>
 
 Returns a hashref with the appropriate icon/link/text for buying this user a paid account.
diff -r 6865fbf1b5da -r 4dedfba1fe16 cgi-bin/DW/Pay.pm
--- a/cgi-bin/DW/Pay.pm	Sat Jul 25 04:43:19 2009 +0000
+++ b/cgi-bin/DW/Pay.pm	Sat Jul 25 04:48:46 2009 +0000
@@ -490,6 +490,12 @@ sub update_paid_status {
             if $dbh->err;
     }
 
+    # and now, at this last step, we kick off a job to check if this user
+    # needs to have their search index setup/messed with.
+    if ( @LJ::SPHINX_SEARCHD && ( my $sclient = LJ::theschwartz() ) ) {
+        $sclient->insert_jobs( TheSchwartz::Job->new_from_array( 'DW::Worker::Sphinx::Copier', { userid => $u->id } ) );
+    }
+
     return 1;
 }
 
diff -r 6865fbf1b5da -r 4dedfba1fe16 cgi-bin/LJ/Entry.pm
--- a/cgi-bin/LJ/Entry.pm	Sat Jul 25 04:43:19 2009 +0000
+++ b/cgi-bin/LJ/Entry.pm	Sat Jul 25 04:48:46 2009 +0000
@@ -1982,6 +1982,11 @@ sub delete_entry
     # delete all comments
     LJ::delete_all_comments($u, 'L', $jitemid);
 
+    # fired to delete the post from the Sphinx search database
+    if ( @LJ::SPHINX_SEARCHD && ( my $sclient = LJ::theschwartz() ) ) {
+        $sclient->insert_jobs( TheSchwartz::Job->new_from_array( 'DW::Worker::Sphinx::Copier', { userid => $u->id } ) );
+    }
+
     return 1;
 }
 
diff -r 6865fbf1b5da -r 4dedfba1fe16 cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Sat Jul 25 04:43:19 2009 +0000
+++ b/cgi-bin/ljprotocol.pl	Sat Jul 25 04:48:46 2009 +0000
@@ -1540,6 +1540,11 @@ sub postevent
     }
     push @jobs, LJ::EventLogRecord::NewEntry->new($entry)->fire_job;
 
+    # update the sphinx search engine
+    if ( @LJ::SPHINX_SEARCHD ) {
+        push @jobs, TheSchwartz::Job->new_from_array( 'DW::Worker::Sphinx::Copier', { userid => $uowner->id } );
+    }
+
     my $sclient = LJ::theschwartz();
     if ($sclient && @jobs) {
         my @handles = $sclient->insert_jobs(@jobs);
@@ -1887,6 +1892,12 @@ sub editevent
 
     my $entry = LJ::Entry->new($ownerid, jitemid => $itemid);
     LJ::EventLogRecord::EditEntry->new($entry)->fire;
+
+    # fired to copy the post over to the Sphinx search database
+    if ( @LJ::SPHINX_SEARCHD && ( my $sclient = LJ::theschwartz() ) ) {
+        $sclient->insert_jobs( TheSchwartz::Job->new_from_array( 'DW::Worker::Sphinx::Copier', { userid => $ownerid } ) );
+    }
+
     LJ::run_hooks("editpost", $entry);
 
     return $res;
diff -r 6865fbf1b5da -r 4dedfba1fe16 htdocs/img/silk/profile/search.png
Binary file htdocs/img/silk/profile/search.png has changed
diff -r 6865fbf1b5da -r 4dedfba1fe16 htdocs/search.bml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/search.bml	Sat Jul 25 04:48:46 2009 +0000
@@ -0,0 +1,167 @@
+<?_c
+#
+# search.bml
+#
+# A very basic search function that allows you to search a given journal in
+# a few particular cases.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+    use strict;
+    use vars qw/ %POST %GET /;
+    use Storable;
+
+    # FIXME: english strip and make the UI a lot better :)
+
+    # if no gearman, bail
+    my $gc = LJ::gearman_client();
+    return "Sorry, content searching is not configured on this server.\n"
+        unless $gc && @LJ::SPHINX_SEARCHD;
+
+    # for pages that require authentication
+    my $remote = LJ::get_remote();
+    return "<?needlogin?>" unless $remote;
+
+    # okay, now let's make sure the data is valid
+    my $su = LJ::load_user( $GET{search_user} || $remote->user );
+    return "Invalid search user.\n" unless $su;
+    return "You can't search that journal!\n"
+        unless $remote->equals( $su ) ||
+               $su->is_community;
+
+    # and make sure we're searching a paid account
+    return "Sorry, searches are only available for paid accounts.\n"
+        unless $su->is_paid;
+
+    # for later
+    my $sulj = $su->ljuser_display;
+    my $suu = $su->user;
+
+    # if they posted we might have to do something
+    if ( LJ::did_post() ) {
+        return "Failauth.\n" unless LJ::check_form_auth();
+        # and make sure we got a query
+        my $q = LJ::strip_html( LJ::trim( $POST{query} ) );
+        return "Query must be shorter than 255 characters, sorry!\n"
+            if length( $q ) > 255;
+        return "Please enter a search query.\n"
+            unless $q;
+
+        # if an offset, less than 1000 please
+        my $offset = $GET{offset} + 0;
+        return "Hey, that offset is nonsensical... :(\n"
+            if $offset < 0 || $offset > 1000;
+
+        # set a public only flag if this is not your journal
+        my $public = ( $remote->equals( $su ) || ( $su->is_community && $remote->member_of( $su ) ) ) ? 0 : 1;
+
+        # the arguments to the search
+        my $args = { userid => $su->id, query => $q, offset => $offset, public => $public };
+        my $arg = Storable::nfreeze( $args );
+
+        # so we know that they're searching something valid, send to gearman
+        my $result;
+        my $task = Gearman::Task->new(
+            'sphinx_search', \$arg,
+            {
+                uniq => '-',
+                on_complete => sub {
+                    my $res = $_[0] or return undef;
+                    $result = Storable::thaw( $$res );
+                },
+            }
+        );
+
+        # setup the task set for gearman... really, isn't there a way to make this
+        # simpler?  oh well
+        my $ts = $gc->new_task_set();
+        $ts->add_task( $task );
+        $ts->wait( timeout => 20 );
+
+        # if we didn't get a result...
+        return "Sorry, we were unable to find a result in the time allotted.  This may mean that ".
+               "the server is busy or down.  Please try your query again later.\n"
+            unless $result;
+
+        # if we didn't get any matches...
+        return "Sorry, we didn't find any matches for the search <strong>$q</strong>.  We looked for $result->{time} seconds, too!\n"
+            if $result->{total} <= 0;
+
+        # now we can process the results and do something fascinating!
+        my $matches = '';
+        foreach my $match ( @{ $result->{matches} } ) {
+            my $icon = {
+                    public => '',
+                    private => "<img src='$LJ::IMGPREFIX/silk/entry/private.png'>",
+                    usemask => "<img src='$LJ::IMGPREFIX/silk/entry/filtered.png'>",
+                    access => "<img src='$LJ::IMGPREFIX/silk/entry/locked.png'>",
+                }->{$match->{security}};
+
+            my $tags = join( ', ', map { "<strong>" . $match->{tags}->{$_} . "</strong>" } keys %{ $match->{tags} } );
+            $tags = "<br />Tags: $tags"
+                if $tags;
+
+            my $html = qq(<div class='searchres'>$icon <a href="$match->{url}">$match->{subject}</a><br />
+                          <span class='exc'>$match->{excerpt}</span>$tags<br />Posted: <strong>$match->{eventtime}</strong><br /><br />
+                          </div>);
+            $matches .= $html;
+        }
+
+        # build the rest of the search page
+        my $ret = "<?p You have searched $sulj... p?>" . $matches;
+
+        # put some stats on the output
+        my $matchct = scalar( @{ $result->{matches} } );
+        my $skip = $offset > 0 ? " (skipped $offset)" : "";
+        $ret .= qq(<span class="stats">$matchct results displayed out of $result->{total} hits total$skip for <strong>$q</strong>.
+                   $result->{time} seconds.</span>);
+
+        if ( $result->{total} > ( $offset + $matchct ) ) {
+            my $offsetm = $offset + $matchct;
+            my $fa = LJ::form_auth();
+            $ret .= qq(<form method="post" action="$LJ::SITEROOT/search?search_user=$suu&offset=$offsetm">$fa
+                       <input type="hidden" name="query" value="$q"> <input type="submit" value="More Results..." />
+                       </form>);
+        }
+
+        return $ret;
+    }
+
+    # give them a form to search...
+    my $fa = LJ::form_auth();
+    return <<EOF;
+
+<?p Searching $sulj ... p?>
+
+<form method="post" action="$LJ::SITEROOT/search?search_user=$suu">
+$fa
+<input type="text" name="query" maxlength="255" size="60"> <input type="submit" value="Search!" />
+</form>
+
+<?p This is a beta search.  Things may be slow or broken while we iron out the kinks
+in the system.  If you run into trouble, let us know! p?>
+
+EOF
+}
+_code?>
+<=body
+title=>Content Search
+head<=
+<style type="text/css">
+.exc { padding-left: 1em; font-style: italic; font-size: smaller; }
+.stats { font-style: italic;  }
+.searchres { margin: 0.2em 0em 0.2em 2em; }
+</style>
+<=head
+page?>
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org