PoShLucene.psm1
using assembly Lucene.Net.dll using namespace System.IO using namespace Lucene.Net.Analysis using namespace Lucene.Net.Analysis.Standard using namespace Lucene.Net.Documents using namespace Lucene.Net.Index using namespace Lucene.Net.QueryParsers using namespace Lucene.Net.Store using namespace Lucene.Net.Util using namespace Lucene.Net.Search using namespace System.Windows.Documents Add-Type -AssemblyName presentationframework Add-Type -AssemblyName System.Windows.Forms function Invoke-PoshLucene { $XAML=@' <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PoShLucene" Width="850" Height="500" Background="#282c34" WindowStartupLocation="CenterScreen"> <Grid> <Grid.Resources> <VisualBrush x:Key="SearchHint" AlignmentX="Left" AlignmentY="Top" Stretch="None"> <VisualBrush.Transform> <TranslateTransform X="5" Y="7" /> </VisualBrush.Transform> <VisualBrush.Visual> <Grid HorizontalAlignment="Left"> <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" FontStyle="Italic" Foreground="Gray" Opacity="1" Text="Search in file contents" /> </Grid> </VisualBrush.Visual> </VisualBrush> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition Height="75" /> <RowDefinition /> <RowDefinition Height="45" /> </Grid.RowDefinitions> <Grid Grid.Row="0" Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="50" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Label Grid.Row="0" Grid.Column="0" Margin="3" Content="_Target" Foreground="#99ffcc" /> <TextBox Name="txtTarget" Grid.Row="0" Grid.Column="1" Margin="3" /> <Label Grid.Row="1" Grid.Column="0" Margin="3" Content="_Query" Foreground="#99ffcc" /> <TextBox x:Name="query" Grid.Row="1" Grid.Column="1" Margin="3"> <TextBox.Style> <Style TargetType="{x:Type TextBox}"> <Setter Property="Background" Value="White" /> <Style.Triggers> <DataTrigger Binding="{Binding ElementName=query, Path=Text}" Value=""> <Setter Property="Background" Value="{StaticResource SearchHint}" /> </DataTrigger> </Style.Triggers> </Style> </TextBox.Style> </TextBox> </Grid> <Grid Grid.Row="1" Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="100" /> <RowDefinition Height="5" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="150" /> </Grid.ColumnDefinitions> <ListBox Name="hits" Grid.Row="0" Grid.Column="0" Margin="5" Background="#282c34" Foreground="#4cd2ff" /> <StackPanel Grid.Row="0" Grid.Column="1" Margin="5"> <Button Name="prevButton" Content="_Previous Occurrence" Margin="0,0,0,5" /> <Button Name="nextButton" Content="_Next Occurrence" /> </StackPanel> <GridSplitter Grid.Row="1" Grid.ColumnSpan="2" Height="3" Margin="5,0,5,0" ResizeDirection="Rows" HorizontalAlignment="Stretch" Background="#ff265c" /> <RichTextBox Name="OutputPane" Grid.Row="2" Grid.ColumnSpan="2" Margin="5" Background="#282c34" Foreground="#ccff99" IsReadOnly="True" FontFamily="Consolas" FontSize="14" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" /> </Grid> <StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal"> <TextBox Name="txtStatus" Margin="8" FontStyle="Italic" Background="#282c34" Foreground="#99ffcc" TextWrapping="Wrap" HorizontalAlignment="Left" BorderThickness="0" /> <Label x:Name="txtPath" Margin="8" FontStyle="Italic" Foreground="#4cd2ff" /> </StackPanel> </Grid> </Window> '@ $Window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader ([xml]$XAML))) $txtTarget = $Window.FindName("txtTarget") $query = $Window.FindName("query") $hits = $Window.FindName("hits") $OutputPane = $Window.FindName("OutputPane") $txtStatus = $Window.FindName("txtStatus") $txtPath = $Window.FindName("txtPath") $nextButton = $Window.FindName("nextButton") $prevButton = $Window.FindName("prevButton") $null = $txtTarget.Focus() $analyzer = [StandardAnalyzer]::new("LUCENE_CURRENT") $directory = [RAMDirectory]::new() $theIndex=@{name='';indexed=$false} $highlightMatchColor = "YellowGreen" $highlightSelectionColor = "Orange" $lastSelectedResult = -1 $lastSelectedResultOccurrence = -1 function FindTextPosition([TextPointer]$startPos, [int]$offset) { $i = 0 $currentPos = $startPos while ($i -lt $offset -and $currentPos) { if ($currentPos.GetPointerContext([LogicalDirection]::Forward) -eq [TextPointerContext]::Text) { $i = $i + 1 } if (!$currentPos.GetPositionAtOffset(1, [LogicalDirection]::Forward)) { return $currentPos } $currentPos = $currentPos.GetPositionAtOffset([LogicalDirection]::Forward) } return $currentPos } function SetBackgroundColor($occurrence, $backgroundColor) { $documentStartPos = $OutputPane.Document.ContentStart $selection = $OutputPane.Selection $selection.Select($occurrence.StartPosition, $occurrence.EndPosition) $selection.ApplyPropertyValue([System.Windows.Documents.TextElement]::BackgroundProperty, $backgroundColor); $selection.Select($documentStartPos, $documentStartPos) } function GetFlowDocument ($resultFile) { if (!$resultFile.FlowDocument) { $run = New-Object System.Windows.Documents.Run $resultFile.Content $paragraph = New-Object System.Windows.Documents.Paragraph $run $OutputPane.Document = New-Object System.Windows.Documents.FlowDocument $paragraph [void]$OutputPane.Focus() $documentStartPos = $OutputPane.Document.ContentStart $lastPos = $documentStartPos $lastOffset = 0 $positions = @() $resultFile.Offsets | ForEach-Object { $startPos = FindTextPosition $lastPos ($_.StartOffset - $lastOffset) $endPos = FindTextPosition $startPos ($_.EndOffset - $_.StartOffset) $occurrence = @{ StartPosition = $startPos; EndPosition = $endPos } SetBackgroundColor $occurrence $highlightMatchColor $positions += $occurrence $lastPos = $endPos $lastOffset = $_.EndOffset } | Out-Null $resultFile.FlowDocument = $OutputPane.Document $resultFile.OccurrencePositions = $positions } return $resultFile.FlowDocument } function ClearSelectedOccurrence() { if ($script:lastSelectedResult -gt -1 -and $script:lastSelectedResultOccurrence -gt -1) { $lastResult = $script:totalDocs[$script:lastSelectedResult] if ($lastResult -and $lastResult.OccurrencePositions) { $lastOccurrence = $lastResult.OccurrencePositions[$script:lastSelectedResultOccurrence] if ($lastOccurrence) { SetBackgroundColor $lastOccurrence $highlightMatchColor } } } } function ShowResult ([int]$resultId, [int]$occurrenceId = 0) { $resultInfo = $script:totalDocs[$resultId] if ($resultInfo) { ClearSelectedOccurrence $OutputPane.Document = GetFlowDocument $resultInfo $txtPath.Content = $resultInfo.Path $occurrence = $resultInfo.OccurrencePositions[$occurrenceId] if ($occurrence) { SetBackgroundColor $occurrence $highlightSelectionColor $occurrence.StartPosition.Parent.BringIntoView() } $script:lastSelectedResult = $resultId $script:lastSelectedResultOccurrence = $occurrenceId } } function BuildResult ([ScoreDoc]$scoredDocument, [string]$queryStr) { $document = $isearcher.Doc($scoredDocument.Doc) $indexReader = [IndexReader]::Open($directory, $true) $termPosVector = $indexReader.GetTermFreqVector($scoredDocument.Doc, "fulltext") $termIndex = $termPosVector.IndexOf($queryStr.ToLower()) $indexReader.Dispose() @{ Path = $document.Get("filepath") Content = $document.Get("fulltext") Document = $document Offsets = $termPosVector.GetOffsets($termIndex) FlowDocument = $null # This will be populated on first display OccurrencePositions = $null # This will be populated on first display } } function DoIndex ($targetFileList) { if($theIndex.name -eq $targetFileList -and $theIndex.index -eq $true) { return } $timing = Measure-Command { $iwriter = [IndexWriter]::new($directory, $analyzer, $true, [IndexWriter+MaxFieldLength]::new(25000)) $count = 0 $cmd = "ls -rec {0} -File | % fullname " -f $targetFileList foreach ($file in ($cmd | Invoke-Expression)) { try { $doc = [Document]::new() $text = [IO.file]::ReadAllText($file) Write-Verbose -Message "Analyzed the file: $file" $doc.Add([Field]::new("fulltext", [string]$text, "YES", "ANALYZED", "WITH_POSITIONS_OFFSETS")) $doc.Add([Field]::new("filepath", $file, "YES", "ANALYZED")) $iwriter.AddDocument($doc) $count++ } catch [Exception] { $errMsg = "Unable to read the file {0} `nException: {1}" -f $file, $_.Exception.ToString() Write-Error $errMsg } } } $txtStatus.text = "{0} files indexed in {1} seconds" -f $count, $timing.TotalSeconds $iwriter.close() $theIndex.name=$targetFileList $theIndex.index=$true } function DoSearch ($q) { try { $timing = Measure-Command { $script:isearcher = [IndexSearcher]::new($directory, $true) # read-only-true $parser = [QueryParser]::new("LUCENE_CURRENT", "fulltext", $analyzer) $query = $parser.Parse($q) $totalHits = $isearcher.Search($query, $null, 1000).ScoreDocs } $totalHits | ForEach-Object { BuildResult $_ $q } $txtStatus.text = "{0}`n{1} hits found in {2} seconds" -f $txtStatus.text, $totalHits.count, $timing.TotalSeconds } catch [Exception] { $errMsg = "Search failed with the following `nException: {0}" -f $_.Exception.ToString() Write-Error $errMsg } } $txtTarget.add_PreviewKeyUp({ param($sender, $keyArgs) if($keyArgs.Key -eq 'Enter') { [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait $txtStatus.text = "[{0}] Indexing..." -f (Get-Date) [System.Windows.Forms.Application]::DoEvents() DoIndex $txtTarget.Text -ErrorAction SilentlyContinue [System.Windows.Input.Mouse]::OverrideCursor = $null } }) $query.add_PreviewKeyUp({ param($sender,$keyArgs) if($keyArgs.Key -eq 'Enter') { if (!$query.Text) { return } [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait if ($txtTarget.Text -ne $null -and ([String]::IsNullOrWhiteSpace($txtTarget.Text) -ne $true)) { DoIndex $txtTarget.Text -ErrorAction SilentlyContinue } $adding=$true $script:totalDocs = @(DoSearch $query.Text -ErrorAction SilentlyContinue) $hits.items.Clear() $OutputPane.Document = New-Object System.Windows.Documents.FlowDocument $txtPath.Content = "" for ($i = 0; $i -lt $totalDocs.count; $i++) { $hits.Items.Add($script:totalDocs[$i].Path) } $adding = $false if($totalDocs.Count -ge 1) { $hits.Focus() $hits.SelectedItem = $hits.Items[0] } [System.Windows.Input.Mouse]::OverrideCursor = $null } }) function ShowNextOccurrence([bool]$moveBackward = $false) { if ($script:totalDocs -eq $null -or $script:totalDocs.Count -eq 0) { return } $nextResultId = $script:lastSelectedResult $nextResultOccurrence = $script:lastSelectedResultOccurrence $currentResult = $script:totalDocs[$script:lastSelectedResult] if ($currentResult) { if ($moveBackward) { # Search backward $nextResultOccurrence -= 1 if ($nextResultOccurrence -eq -1) { $nextResultId = $script:lastSelectedResult - 1 if ($nextResultId -eq -1) { $nextResultId = $script:totalDocs.Count - 1 } $currentResult = $script:totalDocs[$nextResultId] $nextResultOccurrence = $currentResult.Offsets.Count - 1 } } else { # Search forward $nextResultOccurrence += 1 if ($nextResultOccurrence -eq $currentResult.Offsets.Count) { $nextResultOccurrence = 0 $nextResultId = $script:lastSelectedResult + 1 if ($nextResultId -eq $script:totalDocs.Count) { $nextResultId = 0 } } } } $hits.SelectedIndex = $nextResultId ShowResult $nextResultId $nextResultOccurrence $hits.ScrollIntoView($hits.SelectedItem) } $Window.add_PreviewKeyUp({ param($sender, $keyArgs) # Result navigation with F3 if($keyArgs.Key -eq 'F3') { ShowNextOccurrence ([System.Windows.Input.Keyboard]::IsKeyDown("LeftShift")) } }) $nextButton.add_Click({ ShowNextOccurrence $false }) $prevButton.add_Click({ ShowNextOccurrence $true }) $hits.add_SelectionChanged({ if($adding) { return } ShowResult $hits.SelectedIndex $hitDoc }) $txtPath.add_MouseEnter({ $txtPath.Cursor = [System.Windows.Input.Cursors]::Hand }) $txtPath.add_MouseLeftButtonDown({ $fPath = $txtPath.Content if (Test-Path -Path $fPath) { Start-Process -FilePath "$env:windir\explorer.exe" -ArgumentList "/select, ""$fPath""" } }) [void]$Window.ShowDialog() if($script:isearcher) { $script:isearcher.Close() } if($directory) { $directory.Close() } } #Export-ModuleMember -Function Invoke-LuceneSearch |